mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-11 18:10:01 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d0307eac | ||
|
|
ac31042e69 | ||
|
|
c733d3bf4d | ||
|
|
bf1243f4c4 | ||
|
|
15063b2e97 | ||
|
|
fc07f1cd87 | ||
|
|
9246550cc5 | ||
|
|
979b6eae1a | ||
|
|
10da337a9c | ||
|
|
9c5e8857af | ||
|
|
84b4baa99e | ||
|
|
b57d46244a | ||
|
|
5faae8796d | ||
|
|
a0805bc0ce |
25
.github/workflows/pr_agent.yml
vendored
Normal file
25
.github/workflows/pr_agent.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Codium PR Agent
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, ready_for_review]
|
||||||
|
jobs:
|
||||||
|
pr_agent_job:
|
||||||
|
if: ${{ github.event.sender.type != 'Bot' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
name: Run pr agent on every pull request, respond to user comments
|
||||||
|
steps:
|
||||||
|
- name: PR Agent action step
|
||||||
|
id: pragent
|
||||||
|
uses: Codium-ai/pr-agent@main
|
||||||
|
env:
|
||||||
|
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||||
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
|
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||||
|
CONFIG.MODEL: "gpt-4o"
|
||||||
|
CONFIG.MODEL_TURBO: "gpt-4o"
|
||||||
|
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,6 +1,26 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## v0.7.1
|
||||||
|
|
||||||
|
- fix: 修复用户角色加载失败的问题
|
||||||
|
- feat: admin 账号设置增加来源邮件地址黑名单配置
|
||||||
|
|
||||||
|
## v0.7.0
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
DB changes: 增加用户 `passkey` 表, 需要执行 `db/2024-08-10-patch.sql` 更新 `D1` 数据库
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Docs: Update new-address-api.md (#360)
|
||||||
|
- feat: worker 增加 `ADMIN_USER_ROLE` 配置, 用于配置管理员用户角色,此角色的用户可访问 admin 管理页面 (#363)
|
||||||
|
- feat: worker 增加 `DISABLE_SHOW_GITHUB` 配置, 用于配置是否显示 github 链接
|
||||||
|
- feat: worker 增加 `NO_LIMIT_SEND_ROLE` 配置, 用于配置可以无限发送邮件的角色
|
||||||
|
- feat: 用户增加 `passkey` 登录方式, 用于用户登录, 无需输入密码
|
||||||
|
- feat: worker 增加 `DISABLE_ADMIN_PASSWORD_CHECK` 配置, 用于配置是否禁用 admin 控制台密码检查, 若你的网站只可私人访问,可通过此禁用检查
|
||||||
|
|
||||||
## v0.6.1
|
## v0.6.1
|
||||||
|
|
||||||
- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355)
|
- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355)
|
||||||
|
|||||||
14
db/2024-08-10-patch.sql
Normal file
14
db/2024-08-10-patch.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS user_passkeys (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
passkey_name TEXT NOT NULL,
|
||||||
|
passkey_id TEXT NOT NULL,
|
||||||
|
passkey TEXT NOT NULL,
|
||||||
|
counter INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id);
|
||||||
@@ -88,3 +88,18 @@ CREATE TABLE IF NOT EXISTS user_roles (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_passkeys (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
passkey_name TEXT NOT NULL,
|
||||||
|
passkey_id TEXT NOT NULL,
|
||||||
|
passkey TEXT NOT NULL,
|
||||||
|
counter INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "0.6.1",
|
"version": "0.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@unhead/vue": "^1.9.15",
|
"@unhead/vue": "^1.9.15",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
|
|||||||
15
frontend/pnpm-lock.yaml
generated
15
frontend/pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@simplewebauthn/browser':
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0
|
||||||
'@unhead/vue':
|
'@unhead/vue':
|
||||||
specifier: ^1.9.15
|
specifier: ^1.9.15
|
||||||
version: 1.9.15(vue@3.4.31(typescript@5.4.5))
|
version: 1.9.15(vue@3.4.31(typescript@5.4.5))
|
||||||
@@ -1188,6 +1191,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@simplewebauthn/browser@10.0.0':
|
||||||
|
resolution: {integrity: sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==}
|
||||||
|
|
||||||
|
'@simplewebauthn/types@10.0.0':
|
||||||
|
resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||||
|
|
||||||
@@ -4075,6 +4084,12 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.18.0':
|
'@rollup/rollup-win32-x64-msvc@4.18.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@simplewebauthn/browser@10.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@simplewebauthn/types': 10.0.0
|
||||||
|
|
||||||
|
'@simplewebauthn/types@10.0.0': {}
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useGlobalState } from './store'
|
|||||||
import { useIsMobile } from './utils/composables'
|
import { useIsMobile } from './utils/composables'
|
||||||
import Header from './views/Header.vue';
|
import Header from './views/Header.vue';
|
||||||
import Footer from './views/Footer.vue';
|
import Footer from './views/Footer.vue';
|
||||||
|
import { api } from './api'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isDark, loading, useSideMargin, telegramApp, isTelegram
|
isDark, loading, useSideMargin, telegramApp, isTelegram
|
||||||
@@ -19,6 +19,13 @@ const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
|||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.getUserSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN;
|
const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN;
|
||||||
|
|
||||||
const exist = document.querySelector('script[src="https://static.cloudflareinsights.com/beacon.min.js"]') !== null
|
const exist = document.querySelector('script[src="https://static.cloudflareinsights.com/beacon.min.js"]') !== null
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
data: options.body || null,
|
data: options.body || null,
|
||||||
headers: {
|
headers: {
|
||||||
'x-user-token': userJwt.value,
|
'x-user-token': userJwt.value,
|
||||||
|
'x-user-access-token': userSettings.value.access_token,
|
||||||
'x-custom-auth': auth.value,
|
'x-custom-auth': auth.value,
|
||||||
'x-admin-auth': adminAuth.value,
|
'x-admin-auth': adminAuth.value,
|
||||||
'Authorization': `Bearer ${jwt.value}`,
|
'Authorization': `Bearer ${jwt.value}`,
|
||||||
@@ -127,7 +128,7 @@ const getUserSettings = async (message) => {
|
|||||||
const res = await api.fetch("/user_api/settings")
|
const res = await api.fetch("/user_api/settings")
|
||||||
Object.assign(userSettings.value, res)
|
Object.assign(userSettings.value, res)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message?.error(error.message || "error");
|
||||||
} finally {
|
} finally {
|
||||||
userSettings.value.fetched = true;
|
userSettings.value.fetched = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const refresh = async () => {
|
|||||||
const { results, count: totalCount } = await props.fetchMailData(
|
const { results, count: totalCount } = await props.fetchMailData(
|
||||||
pageSize.value, (page.value - 1) * pageSize.value
|
pageSize.value, (page.value - 1) * pageSize.value
|
||||||
);
|
);
|
||||||
|
loading.value = true;
|
||||||
data.value = await Promise.all(results.map(async (item) => {
|
data.value = await Promise.all(results.map(async (item) => {
|
||||||
item.checked = false;
|
item.checked = false;
|
||||||
return await processItem(item);
|
return await processItem(item);
|
||||||
@@ -161,6 +162,8 @@ const refresh = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
|
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
|
||||||
|
|
||||||
export const useGlobalState = createGlobalState(
|
export const useGlobalState = createGlobalState(
|
||||||
@@ -25,6 +25,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
cfTurnstileSiteKey: '',
|
cfTurnstileSiteKey: '',
|
||||||
enableWebhook: false,
|
enableWebhook: false,
|
||||||
isS3Enabled: false,
|
isS3Enabled: false,
|
||||||
|
showGithub: true,
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
@@ -74,9 +75,14 @@ export const useGlobalState = createGlobalState(
|
|||||||
user_email: '',
|
user_email: '',
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
|
/** @type {boolean} */
|
||||||
|
is_admin: false,
|
||||||
|
/** @type {string | null} */
|
||||||
|
access_token: null,
|
||||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||||
user_role: null,
|
user_role: null,
|
||||||
});
|
});
|
||||||
|
const showAdminPage = computed(() => !!adminAuth.value || userSettings.value.is_admin);
|
||||||
const telegramApp = ref(window.Telegram?.WebApp || {});
|
const telegramApp = ref(window.Telegram?.WebApp || {});
|
||||||
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||||
return {
|
return {
|
||||||
@@ -108,6 +114,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
useSideMargin,
|
useSideMargin,
|
||||||
telegramApp,
|
telegramApp,
|
||||||
isTelegram,
|
isTelegram,
|
||||||
|
showAdminPage,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import Telegram from './admin/Telegram.vue';
|
|||||||
import Webhook from './admin/Webhook.vue';
|
import Webhook from './admin/Webhook.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
adminAuth, showAdminAuth, adminTab, loading,
|
||||||
|
globalTabplacement, showAdminPage
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
if (!showAdminPage.value) {
|
||||||
showAdminAuth.value = true;
|
showAdminAuth.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-tabs type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
<n-tabs type="bar" animated>
|
<n-tabs type="bar" animated>
|
||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { getRouterPathWithLang } from '../utils'
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
toggleDark, isDark, isTelegram,
|
toggleDark, isDark, isTelegram, showAdminPage,
|
||||||
showAuth, adminAuth, auth, loading, openSettings
|
showAuth, auth, loading, openSettings, userSettings
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -134,7 +134,7 @@ const menuOptions = computed(() => [
|
|||||||
icon: () => h(NIcon, { component: AdminPanelSettingsFilled }),
|
icon: () => h(NIcon, { component: AdminPanelSettingsFilled }),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
show: !!adminAuth.value,
|
show: showAdminPage.value,
|
||||||
key: "admin"
|
key: "admin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,6 +192,7 @@ const menuOptions = computed(() => [
|
|||||||
icon: () => h(NIcon, { component: GithubAlt })
|
icon: () => h(NIcon, { component: GithubAlt })
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
show: openSettings.value?.showGithub,
|
||||||
key: "github"
|
key: "github"
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
@@ -223,6 +224,8 @@ const logoClick = async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getOpenSettings(message);
|
await api.getOpenSettings(message);
|
||||||
|
// make sure user_id is fetched
|
||||||
|
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { NButton, NMenu } from 'naive-ui';
|
|||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adminAuth, showAdminAuth, loading,
|
showAdminAuth, loading, adminTab,
|
||||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
adminMailTabAddress, adminSendBoxTabAddress
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -252,10 +252,6 @@ watch([page, pageSize], async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchData()
|
await fetchData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,20 +11,24 @@ const message = useMessage()
|
|||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
|
tip: 'You can manually input the following multiple select input',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
successTip: 'Save Success',
|
successTip: 'Save Success',
|
||||||
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||||
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
||||||
send_address_block_list: 'Address Block Keywords for send email',
|
send_address_block_list: 'Address Block Keywords for send email',
|
||||||
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
|
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
|
||||||
|
fromBlockList: 'Block Keywords for receive email',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
tip: '您可以手动输入以下多选输入框',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
successTip: '保存成功',
|
successTip: '保存成功',
|
||||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||||
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
||||||
send_address_block_list: '发送邮件地址屏蔽关键词',
|
send_address_block_list: '发送邮件地址屏蔽关键词',
|
||||||
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
||||||
|
fromBlockList: '接收邮件地址屏蔽关键词',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -32,6 +36,7 @@ const { t } = useI18n({
|
|||||||
const addressBlockList = ref([])
|
const addressBlockList = ref([])
|
||||||
const sendAddressBlockList = ref([])
|
const sendAddressBlockList = ref([])
|
||||||
const verifiedAddressList = ref([])
|
const verifiedAddressList = ref([])
|
||||||
|
const fromBlockList = ref([])
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -39,6 +44,7 @@ const fetchData = async () => {
|
|||||||
addressBlockList.value = res.blockList || []
|
addressBlockList.value = res.blockList || []
|
||||||
sendAddressBlockList.value = res.sendBlockList || []
|
sendAddressBlockList.value = res.sendBlockList || []
|
||||||
verifiedAddressList.value = res.verifiedAddressList || []
|
verifiedAddressList.value = res.verifiedAddressList || []
|
||||||
|
fromBlockList.value = res.fromBlockList || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,8 @@ const save = async () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
blockList: addressBlockList.value || [],
|
blockList: addressBlockList.value || [],
|
||||||
sendBlockList: sendAddressBlockList.value || [],
|
sendBlockList: sendAddressBlockList.value || [],
|
||||||
verifiedAddressList: verifiedAddressList.value || []
|
verifiedAddressList: verifiedAddressList.value || [],
|
||||||
|
fromBlockList: fromBlockList.value || [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
message.success(t('successTip'))
|
message.success(t('successTip'))
|
||||||
@@ -69,6 +76,9 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
|
<n-alert :show-icon="false" style="margin-bottom: 10px;">
|
||||||
|
{{ t("tip") }}
|
||||||
|
</n-alert>
|
||||||
<n-form-item-row :label="t('address_block_list')">
|
<n-form-item-row :label="t('address_block_list')">
|
||||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')" />
|
:placeholder="t('address_block_list_placeholder')" />
|
||||||
@@ -81,6 +91,9 @@ onMounted(async () => {
|
|||||||
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
||||||
:placeholder="t('verified_address_list')" />
|
:placeholder="t('verified_address_list')" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('fromBlockList')">
|
||||||
|
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
|
||||||
|
</n-form-item-row>
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
<n-button @click="save" type="primary" block :loading="loading">
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import MailBox from '../../components/MailBox.vue';
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
const {
|
const { adminMailTabAddress } = useGlobalState()
|
||||||
adminAuth, showAdminAuth,
|
|
||||||
adminMailTabAddress
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -48,13 +45,6 @@ const fetchMailData = async (limit, offset) => {
|
|||||||
const deleteMail = async (curMailId) => {
|
const deleteMail = async (curMailId) => {
|
||||||
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import MailBox from '../../components/MailBox.vue';
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
const { adminAuth, showAdminAuth } = useGlobalState()
|
|
||||||
|
|
||||||
const fetchMailUnknowData = async (limit, offset) => {
|
const fetchMailUnknowData = async (limit, offset) => {
|
||||||
return await api.fetch(
|
return await api.fetch(
|
||||||
`/admin/mails_unknow`
|
`/admin/mails_unknow`
|
||||||
@@ -18,17 +13,10 @@ const fetchMailUnknowData = async (limit, offset) => {
|
|||||||
const deleteMail = async (curMailId) => {
|
const deleteMail = async (curMailId) => {
|
||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="adminAuth" style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<MailBox :enableUserDeleteEmail="true" :fetchMailData="fetchMailUnknowData" :deleteMail="deleteMail" />
|
<MailBox :enableUserDeleteEmail="true" :fetchMailData="fetchMailUnknowData" :deleteMail="deleteMail" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { CleaningServicesFilled } from '@vicons/material'
|
import { CleaningServicesFilled } from '@vicons/material'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
const { adminAuth, showAdminAuth } = useGlobalState()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const cleanupModel = ref({
|
const cleanupModel = ref({
|
||||||
enableMailsAutoCleanup: false,
|
enableMailsAutoCleanup: false,
|
||||||
@@ -80,10 +78,6 @@ const save = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchData();
|
await fetchData();
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||||
import { SendOutlined } from '@vicons/material'
|
import { SendOutlined } from '@vicons/material'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
const { adminAuth } = useGlobalState()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -60,9 +58,6 @@ const fetchStatistics = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchStatistics()
|
await fetchStatistics()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { api } from '../../api'
|
|||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
import { processItem } from '../../utils/email-parser'
|
import { processItem } from '../../utils/email-parser'
|
||||||
|
|
||||||
const { telegramApp } = useGlobalState()
|
const { telegramApp, loading } = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const curMail = ref({});
|
const curMail = ref({});
|
||||||
@@ -26,12 +26,16 @@ const fetchMailData = async () => {
|
|||||||
mailId: route.query.mail_id
|
mailId: route.query.mail_id
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
loading.value = true;
|
||||||
return await processItem(res);
|
return await processItem(res);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const { t } = useI18n({
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getUserOpenSettings(message);
|
await api.getUserOpenSettings(message);
|
||||||
await api.getUserSettings(message);
|
// make sure user_id is fetched
|
||||||
|
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { onMounted, ref } from "vue";
|
||||||
import { computed, onMounted, ref } from "vue";
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { api } from '../../api';
|
import { api } from '../../api';
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
|
import { startAuthentication } from '@simplewebauthn/browser';
|
||||||
|
|
||||||
import Turnstile from '../../components/Turnstile.vue';
|
import Turnstile from '../../components/Turnstile.vue';
|
||||||
|
|
||||||
const { userJwt, userTab, userOpenSettings, openSettings } = useGlobalState()
|
const { userJwt, userOpenSettings, openSettings } = useGlobalState()
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -33,6 +32,7 @@ const { t } = useI18n({
|
|||||||
pleaseInputCode: 'Please input code',
|
pleaseInputCode: 'Please input code',
|
||||||
pleaseCompleteTurnstile: 'Please complete turnstile',
|
pleaseCompleteTurnstile: 'Please complete turnstile',
|
||||||
pleaseLogin: 'Please login',
|
pleaseLogin: 'Please login',
|
||||||
|
loginWithPasskey: 'Login with Passkey',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@@ -51,6 +51,7 @@ const { t } = useI18n({
|
|||||||
pleaseInputCode: '请输入验证码',
|
pleaseInputCode: '请输入验证码',
|
||||||
pleaseCompleteTurnstile: '请完成人机验证',
|
pleaseCompleteTurnstile: '请完成人机验证',
|
||||||
pleaseLogin: '请登录',
|
pleaseLogin: '请登录',
|
||||||
|
loginWithPasskey: '使用 Passkey 登录',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -156,6 +157,33 @@ const emailSignup = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const passkeyLogin = async () => {
|
||||||
|
try {
|
||||||
|
const options = await api.fetch(`/user_api/passkey/authenticate_request`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
domain: location.hostname,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const credential = await startAuthentication(options)
|
||||||
|
|
||||||
|
// Send the result to the server and return the promise.
|
||||||
|
const res = await api.fetch(`/user_api/passkey/authenticate_response`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
origin: location.origin,
|
||||||
|
domain: location.hostname,
|
||||||
|
credential
|
||||||
|
})
|
||||||
|
})
|
||||||
|
userJwt.value = res.jwt;
|
||||||
|
location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -178,6 +206,10 @@ onMounted(async () => {
|
|||||||
<n-button @click="showModal = true" type="info" quaternary size="tiny">
|
<n-button @click="showModal = true" type="info" quaternary size="tiny">
|
||||||
{{ t('forgotPassword') }}
|
{{ t('forgotPassword') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-divider />
|
||||||
|
<n-button @click="passkeyLogin" type="primary" block secondary strong>
|
||||||
|
{{ t('loginWithPasskey') }}
|
||||||
|
</n-button>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane v-if="userOpenSettings.enable" name="signup" :tab="t('register')">
|
<n-tab-pane v-if="userOpenSettings.enable" name="signup" :tab="t('register')">
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
|
import { NButton, NPopconfirm } from 'naive-ui'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
const { userJwt, userSettings, } = useGlobalState()
|
const { userJwt, userSettings, } = useGlobalState()
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
|
const showCreatePasskey = ref(false)
|
||||||
|
const passkeyName = ref('')
|
||||||
|
const showPasskeyList = ref(false)
|
||||||
|
const showRenamePasskey = ref(false)
|
||||||
|
const currentPasskeyId = ref(null)
|
||||||
|
const currentPasskeyName = ref('')
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -18,11 +24,35 @@ const { t } = useI18n({
|
|||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
logoutConfirm: 'Are you sure you want to logout?',
|
logoutConfirm: 'Are you sure you want to logout?',
|
||||||
passordTip: 'The server will only receive the hash value of the password, and will not receive the plaintext password, so it cannot view or retrieve your password. If the administrator enables email verification, you can reset the password in incognito mode',
|
passordTip: 'The server will only receive the hash value of the password, and will not receive the plaintext password, so it cannot view or retrieve your password. If the administrator enables email verification, you can reset the password in incognito mode',
|
||||||
|
createPasskey: 'Create Passkey',
|
||||||
|
showPasskeyList: 'Show Passkey List',
|
||||||
|
passkeyCreated: 'Passkey created successfully',
|
||||||
|
passkeyNamePlaceholder: 'Please enter the passkey name or leave it empty to generate a random one',
|
||||||
|
renamePasskey: 'Rename Passkey',
|
||||||
|
deletePasskey: 'Delete Passkey',
|
||||||
|
passkey_name: 'Passkey Name',
|
||||||
|
created_at: 'Created At',
|
||||||
|
updated_at: 'Updated At',
|
||||||
|
actions: 'Actions',
|
||||||
|
renamePasskey: 'Rename Passkey',
|
||||||
|
renamePasskeyNamePlaceholder: 'Please enter the new passkey name',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
logoutConfirm: '确定要退出登录吗?',
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
passordTip: '服务器只会接收到密码的哈希值,不会接收到明文密码,因此无法查看或者找回您的密码, 如果管理员启用了邮件验证您可以在无痕模式重置密码',
|
passordTip: '服务器只会接收到密码的哈希值,不会接收到明文密码,因此无法查看或者找回您的密码, 如果管理员启用了邮件验证您可以在无痕模式重置密码',
|
||||||
|
createPasskey: '创建 Passkey',
|
||||||
|
showPasskeyList: '查看 Passkey 列表',
|
||||||
|
passkeyCreated: 'Passkey 创建成功',
|
||||||
|
passkeyNamePlaceholder: '请输入 Passkey 名称或者留空自动生成',
|
||||||
|
renamePasskey: '重命名 Passkey',
|
||||||
|
deletePasskey: '删除 Passkey',
|
||||||
|
passkey_name: 'Passkey 名称',
|
||||||
|
created_at: '创建时间',
|
||||||
|
updated_at: '更新时间',
|
||||||
|
actions: '操作',
|
||||||
|
renamePasskey: '重命名 Passkey',
|
||||||
|
renamePasskeyNamePlaceholder: '请输入新的 Passkey 名称',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -33,17 +63,144 @@ const logout = async () => {
|
|||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const createPasskey = async () => {
|
||||||
|
try {
|
||||||
|
const options = await api.fetch(`/user_api/passkey/register_request`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
domain: location.hostname,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const credential = await startRegistration(options)
|
||||||
|
|
||||||
|
// Send the result to the server and return the promise.
|
||||||
|
await api.fetch(`/user_api/passkey/register_response`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
origin: location.origin,
|
||||||
|
passkey_name: passkeyName.value || (
|
||||||
|
(window.navigator.userAgentData?.platform || "Unknown")
|
||||||
|
+ ": " + Math.random().toString(36).substring(7)
|
||||||
|
),
|
||||||
|
credential
|
||||||
|
})
|
||||||
|
})
|
||||||
|
message.success(t('passkeyCreated'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
passkeyName.value = ''
|
||||||
|
showCreatePasskey.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
const passkeyColumns = [
|
||||||
await fetchData()
|
{
|
||||||
})
|
title: "Passkey ID",
|
||||||
|
key: "passkey_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('passkey_name'),
|
||||||
|
key: "passkey_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('created_at'),
|
||||||
|
key: "created_at"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('updated_at'),
|
||||||
|
key: "updated_at"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('actions'),
|
||||||
|
key: 'actions',
|
||||||
|
render(row) {
|
||||||
|
return h('div', [
|
||||||
|
[
|
||||||
|
h(NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
type: "primary",
|
||||||
|
onClick: () => {
|
||||||
|
showRenamePasskey.value = true;
|
||||||
|
currentPasskeyId.value = row.passkey_id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('renamePasskey') }
|
||||||
|
),
|
||||||
|
h(NPopconfirm,
|
||||||
|
{
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/user_api/passkey/${row.passkey_id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
await fetchPasskeyList()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: () => h(NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
{ default: () => t('deletePasskey') }
|
||||||
|
),
|
||||||
|
default: () => `${t('deletePasskey')}?`
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const passkeyData = ref([])
|
||||||
|
|
||||||
|
const fetchPasskeyList = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.fetch(`/user_api/passkey`)
|
||||||
|
passkeyData.value = data
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renamePasskey = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/user_api/passkey/rename`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
passkey_name: currentPasskeyName.value,
|
||||||
|
passkey_id: currentPasskeyId.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await fetchPasskeyList()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
currentPasskeyName.value = ''
|
||||||
|
showRenamePasskey.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="userSettings.user_email">
|
<div class="center" v-if="userSettings.user_email">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
|
<n-button @click="showPasskeyList = true; fetchPasskeyList();" secondary block strong>
|
||||||
|
{{ t('showPasskeyList') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showCreatePasskey = true" type="primary" secondary block strong>
|
||||||
|
{{ t('createPasskey') }}
|
||||||
|
</n-button>
|
||||||
<n-alert :show-icon="false" :bordered="false">
|
<n-alert :show-icon="false" :bordered="false">
|
||||||
<span>
|
<span>
|
||||||
{{ t('passordTip') }}
|
{{ t('passordTip') }}
|
||||||
@@ -53,6 +210,25 @@ onMounted(async () => {
|
|||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
<n-modal v-model:show="showCreatePasskey" preset="dialog" :title="t('createPasskey')">
|
||||||
|
<n-input v-model:value="passkeyName" :placeholder="t('passkeyNamePlaceholder')" />
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="createPasskey" size="small" tertiary type="primary">
|
||||||
|
{{ t('createPasskey') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showRenamePasskey" preset="dialog" :title="t('renamePasskey')">
|
||||||
|
<n-input v-model:value="currentPasskeyName" :placeholder="t('renamePasskeyNamePlaceholder')" />
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="renamePasskey" size="small" tertiary type="primary">
|
||||||
|
{{ t('renamePasskey') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showPasskeyList" preset="card" :title="t('showPasskeyList')">
|
||||||
|
<n-data-table :columns="passkeyColumns" :data="passkeyData" :bordered="false" embedded />
|
||||||
|
</n-modal>
|
||||||
<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>
|
||||||
@@ -78,5 +254,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.n-button {
|
.n-button {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin console password, if not configured, access to the console is not allowed
|
# admin console password, if not configured, access to the console is not allowed
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# warning: no password or user check for admin portal
|
||||||
|
# DISABLE_ADMIN_PASSWORD_CHECK = false
|
||||||
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
|
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
|
||||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||||
@@ -91,6 +93,7 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all your domain name
|
|||||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||||
|
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||||
# USER_ROLES = [
|
# USER_ROLES = [
|
||||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||||
@@ -108,13 +111,15 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# ENABLE_WEBHOOK = true
|
# ENABLE_WEBHOOK = true
|
||||||
# Footer text
|
# Footer text
|
||||||
# COPYRIGHT = "Dream Hunter"
|
# COPYRIGHT = "Dream Hunter"
|
||||||
|
# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link
|
||||||
# default send balance, if not set, it will be 0
|
# default send balance, if not set, it will be 0
|
||||||
# DEFAULT_SEND_BALANCE = 1
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
|
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||||
# Turnstile verification configuration
|
# Turnstile verification configuration
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# telegram bot
|
# telegram bot
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ADDRESS = 5
|
||||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin 控制台密码, 不配置则不允许访问控制台
|
# admin 控制台密码, 不配置则不允许访问控制台
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# 警告: 管理员控制台没有密码或用户检查
|
||||||
|
# DISABLE_ADMIN_PASSWORD_CHECK = false
|
||||||
# admin 联系方式,不配置则不显示,可配置任意字符串
|
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
|
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
|
||||||
@@ -60,6 +62,8 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
|||||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||||
# 新用户默认角色, 仅在启用邮件验证时有效
|
# 新用户默认角色, 仅在启用邮件验证时有效
|
||||||
# USER_DEFAULT_ROLE = "vip"
|
# USER_DEFAULT_ROLE = "vip"
|
||||||
|
# admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
|
||||||
|
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||||
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
||||||
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
||||||
# USER_ROLES = [
|
# USER_ROLES = [
|
||||||
@@ -78,13 +82,15 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# ENABLE_WEBHOOK = true
|
# ENABLE_WEBHOOK = true
|
||||||
# 前端界面页脚文本
|
# 前端界面页脚文本
|
||||||
# COPYRIGHT = "Dream Hunter"
|
# COPYRIGHT = "Dream Hunter"
|
||||||
|
# DISABLE_SHOW_GITHUB = true # 是否显示 GitHub 链接
|
||||||
# 默认发送邮件余额,如果不设置,将为 0
|
# 默认发送邮件余额,如果不设置,将为 0
|
||||||
# DEFAULT_SEND_BALANCE = 1
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
|
# NO_LIMIT_SEND_ROLE = "vip" # 可以无限发送邮件的角色
|
||||||
# Turnstile 人机验证配置
|
# Turnstile 人机验证配置
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# telegram bot 最多绑定邮箱数量
|
# telegram bot 最多绑定邮箱数量
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ADDRESS = 5
|
||||||
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
||||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# Admin 用户相关
|
# Admin 用户相关
|
||||||
|
|
||||||
默认不允许用户注册,可通过
|
|
||||||
|
|
||||||
## 用户管理页面
|
## 用户管理页面
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
# Admin 控制台
|
# Admin 控制台
|
||||||
|
|
||||||
部署前端应用之后,访问 `/admin` 路径即可进入管理控制台。
|
> [!NOTE]
|
||||||
|
> 需要配置 `ADMIN_PASSWORDS` 或者 `ADMIN_USER_ROLE` 才可以访问 admin 控制台
|
||||||
|
> admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
|
||||||
|
|
||||||
需要在后端配置 `admin 控制台密码`, 不配置则不允许访问控制台。
|
部署前端应用之后,点击 左上角 logo 5 次 或者访问 `/admin` 路径即可进入管理控制台。
|
||||||
|
|
||||||
|
需要在后端配置 `ADMIN_PASSWORDS` 或者当前用户角色为 `ADMIN_USER_ROLE`, 则不允许访问控制台。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 如果你的网站只可私人访问,可通过此禁用检查
|
||||||
|
|
||||||
|
`DISABLE_ADMIN_PASSWORD_CHECK = true`
|
||||||
|
|||||||
@@ -23,3 +23,70 @@ res = requests.post(
|
|||||||
# 返回值 {"jwt": "<Jwt>"}
|
# 返回值 {"jwt": "<Jwt>"}
|
||||||
print(res.json())
|
print(res.json())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# 批量创建随机用户名邮箱地址 API 示例
|
||||||
|
|
||||||
|
## 通过 admin API 批量新建邮箱地址
|
||||||
|
|
||||||
|
这是一个 `python` 的例子,使用 `requests` 库发送邮件。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_name():
|
||||||
|
# 生成5位英文字符
|
||||||
|
letters1 = ''.join(random.choices(string.ascii_lowercase, k=5))
|
||||||
|
# 生成1-3个数字
|
||||||
|
numbers = ''.join(random.choices(string.digits, k=random.randint(1, 3)))
|
||||||
|
# 生成1-3个英文字符
|
||||||
|
letters2 = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3)))
|
||||||
|
# 组合成最终名称
|
||||||
|
return letters1 + numbers + letters2
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_email_data(name):
|
||||||
|
try:
|
||||||
|
res = requests.post(
|
||||||
|
"https://<worker 域名>",
|
||||||
|
json={
|
||||||
|
"enablePrefix": True,
|
||||||
|
"name": name,
|
||||||
|
"domain": "<邮箱域名>",
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
'x-admin-auth': "<你的网站admin密码>",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.status_code == 200:
|
||||||
|
response_data = res.json()
|
||||||
|
email = response_data.get("address", "无地址")
|
||||||
|
jwt = response_data.get("jwt", "无jwt")
|
||||||
|
return f"{email}----{jwt}\n"
|
||||||
|
else:
|
||||||
|
print(f"请求失败,状态码: {res.status_code}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"请求出现错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_and_save_emails(num_emails):
|
||||||
|
with ThreadPoolExecutor(max_workers=30) as executor, open('email.txt', 'a') as file:
|
||||||
|
futures = [executor.submit(fetch_email_data, generate_random_name()) for _ in range(num_emails)]
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
result = future.result()
|
||||||
|
if result:
|
||||||
|
file.write(result)
|
||||||
|
|
||||||
|
|
||||||
|
# 生成10个邮箱并追加到现有文件
|
||||||
|
generate_and_save_emails(10)
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||||
|
|
||||||
|
::: warning 注意
|
||||||
|
下面输入的是 `db/schema.sql` 的内容
|
||||||
|
:::
|
||||||
|
|
||||||
打开 `Console` 标签页,输入 `db/schema.sql` 的内容,点击 `Execute` 执行
|
打开 `Console` 标签页,输入 `db/schema.sql` 的内容,点击 `Execute` 执行
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@eslint/js": "8.56.0",
|
"@eslint/js": "8.56.0",
|
||||||
|
"@simplewebauthn/types": "^10.0.0",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"globals": "^15.8.0",
|
"globals": "^15.8.0",
|
||||||
"typescript-eslint": "^7.15.0",
|
"typescript-eslint": "^7.15.0",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.609.0",
|
"@aws-sdk/client-s3": "^3.609.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.609.0",
|
"@aws-sdk/s3-request-presigner": "^3.609.0",
|
||||||
|
"@simplewebauthn/server": "^10.0.1",
|
||||||
"hono": "^4.4.12",
|
"hono": "^4.4.12",
|
||||||
"mimetext": "^3.0.24",
|
"mimetext": "^3.0.24",
|
||||||
"postal-mime": "^2.2.5",
|
"postal-mime": "^2.2.5",
|
||||||
|
|||||||
126
worker/pnpm-lock.yaml
generated
126
worker/pnpm-lock.yaml
generated
@@ -19,6 +19,9 @@ importers:
|
|||||||
'@aws-sdk/s3-request-presigner':
|
'@aws-sdk/s3-request-presigner':
|
||||||
specifier: ^3.609.0
|
specifier: ^3.609.0
|
||||||
version: 3.609.0
|
version: 3.609.0
|
||||||
|
'@simplewebauthn/server':
|
||||||
|
specifier: ^10.0.1
|
||||||
|
version: 10.0.1
|
||||||
hono:
|
hono:
|
||||||
specifier: ^4.4.12
|
specifier: ^4.4.12
|
||||||
version: 4.4.12
|
version: 4.4.12
|
||||||
@@ -41,6 +44,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: 8.56.0
|
specifier: 8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
|
'@simplewebauthn/types':
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.56.0
|
specifier: 8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
@@ -444,6 +450,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@hexagon/base64@1.1.28':
|
||||||
|
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -471,6 +480,9 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@levischuck/tiny-cbor@0.2.2':
|
||||||
|
resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -486,6 +498,21 @@ packages:
|
|||||||
'@one-ini/wasm@0.1.1':
|
'@one-ini/wasm@0.1.1':
|
||||||
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-android@2.3.10':
|
||||||
|
resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-ecc@2.3.8':
|
||||||
|
resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-rsa@2.3.8':
|
||||||
|
resolution: {integrity: sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-schema@2.3.8':
|
||||||
|
resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509@2.3.8':
|
||||||
|
resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -497,6 +524,13 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
|
'@simplewebauthn/server@10.0.1':
|
||||||
|
resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@simplewebauthn/types@10.0.0':
|
||||||
|
resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
|
||||||
|
|
||||||
'@smithy/abort-controller@3.1.1':
|
'@smithy/abort-controller@3.1.1':
|
||||||
resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==}
|
resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -825,6 +859,10 @@ packages:
|
|||||||
as-table@1.0.55:
|
as-table@1.0.55:
|
||||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||||
|
|
||||||
|
asn1js@3.0.5:
|
||||||
|
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
@@ -900,6 +938,9 @@ packages:
|
|||||||
core-js-pure@3.37.1:
|
core-js-pure@3.37.1:
|
||||||
resolution: {integrity: sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==}
|
resolution: {integrity: sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==}
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||||
|
|
||||||
cross-spawn@7.0.3:
|
cross-spawn@7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1157,6 +1198,10 @@ packages:
|
|||||||
ini@1.3.8:
|
ini@1.3.8:
|
||||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
|
ipaddr.js@2.2.0:
|
||||||
|
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
is-binary-path@2.1.0:
|
is-binary-path@2.1.0:
|
||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1418,6 +1463,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
pvtsutils@1.3.5:
|
||||||
|
resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==}
|
||||||
|
|
||||||
|
pvutils@1.1.3:
|
||||||
|
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -2344,6 +2396,8 @@ snapshots:
|
|||||||
|
|
||||||
'@fastify/busboy@2.1.1': {}
|
'@fastify/busboy@2.1.1': {}
|
||||||
|
|
||||||
|
'@hexagon/base64@1.1.28': {}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
@@ -2374,6 +2428,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
|
'@levischuck/tiny-cbor@0.2.2': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -2388,6 +2444,40 @@ snapshots:
|
|||||||
|
|
||||||
'@one-ini/wasm@0.1.1': {}
|
'@one-ini/wasm@0.1.1': {}
|
||||||
|
|
||||||
|
'@peculiar/asn1-android@2.3.10':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.8
|
||||||
|
asn1js: 3.0.5
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
|
'@peculiar/asn1-ecc@2.3.8':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.8
|
||||||
|
'@peculiar/asn1-x509': 2.3.8
|
||||||
|
asn1js: 3.0.5
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
|
'@peculiar/asn1-rsa@2.3.8':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.8
|
||||||
|
'@peculiar/asn1-x509': 2.3.8
|
||||||
|
asn1js: 3.0.5
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
|
'@peculiar/asn1-schema@2.3.8':
|
||||||
|
dependencies:
|
||||||
|
asn1js: 3.0.5
|
||||||
|
pvtsutils: 1.3.5
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509@2.3.8':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.8
|
||||||
|
asn1js: 3.0.5
|
||||||
|
ipaddr.js: 2.2.0
|
||||||
|
pvtsutils: 1.3.5
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2404,6 +2494,22 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
|
'@simplewebauthn/server@10.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@hexagon/base64': 1.1.28
|
||||||
|
'@levischuck/tiny-cbor': 0.2.2
|
||||||
|
'@peculiar/asn1-android': 2.3.10
|
||||||
|
'@peculiar/asn1-ecc': 2.3.8
|
||||||
|
'@peculiar/asn1-rsa': 2.3.8
|
||||||
|
'@peculiar/asn1-schema': 2.3.8
|
||||||
|
'@peculiar/asn1-x509': 2.3.8
|
||||||
|
'@simplewebauthn/types': 10.0.0
|
||||||
|
cross-fetch: 4.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
|
'@simplewebauthn/types@10.0.0': {}
|
||||||
|
|
||||||
'@smithy/abort-controller@3.1.1':
|
'@smithy/abort-controller@3.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 3.3.0
|
'@smithy/types': 3.3.0
|
||||||
@@ -2871,6 +2977,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
printable-characters: 1.0.42
|
printable-characters: 1.0.42
|
||||||
|
|
||||||
|
asn1js@3.0.5:
|
||||||
|
dependencies:
|
||||||
|
pvtsutils: 1.3.5
|
||||||
|
pvutils: 1.1.3
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
@@ -2948,6 +3060,12 @@ snapshots:
|
|||||||
|
|
||||||
core-js-pure@3.37.1: {}
|
core-js-pure@3.37.1: {}
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
cross-spawn@7.0.3:
|
cross-spawn@7.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -3258,6 +3376,8 @@ snapshots:
|
|||||||
|
|
||||||
ini@1.3.8: {}
|
ini@1.3.8: {}
|
||||||
|
|
||||||
|
ipaddr.js@2.2.0: {}
|
||||||
|
|
||||||
is-binary-path@2.1.0:
|
is-binary-path@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.3.0
|
binary-extensions: 2.3.0
|
||||||
@@ -3483,6 +3603,12 @@ snapshots:
|
|||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
pvtsutils@1.3.5:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
|
pvutils@1.1.3: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
react-dom@18.3.1(react@18.3.1):
|
react-dom@18.3.1(react@18.3.1):
|
||||||
|
|||||||
@@ -246,10 +246,12 @@ api.get('/admin/account_settings', async (c) => {
|
|||||||
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||||
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
|
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
|
||||||
|
const fromBlockList = c.env.KV ? await c.env.KV.get<string[]>(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') : [];
|
||||||
return c.json({
|
return c.json({
|
||||||
blockList: blockList || [],
|
blockList: blockList || [],
|
||||||
sendBlockList: sendBlockList || [],
|
sendBlockList: sendBlockList || [],
|
||||||
verifiedAddressList: verifiedAddressList || []
|
verifiedAddressList: verifiedAddressList || [],
|
||||||
|
fromBlockList: fromBlockList || []
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -259,7 +261,7 @@ api.get('/admin/account_settings', async (c) => {
|
|||||||
|
|
||||||
api.post('/admin/account_settings', async (c) => {
|
api.post('/admin/account_settings', async (c) => {
|
||||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||||
const { blockList, sendBlockList, verifiedAddressList } = await c.req.json();
|
const { blockList, sendBlockList, verifiedAddressList, fromBlockList } = await c.req.json();
|
||||||
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
||||||
return c.text("Invalid blockList or sendBlockList", 400)
|
return c.text("Invalid blockList or sendBlockList", 400)
|
||||||
}
|
}
|
||||||
@@ -278,6 +280,12 @@ api.post('/admin/account_settings', async (c) => {
|
|||||||
c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY,
|
c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY,
|
||||||
JSON.stringify(verifiedAddressList)
|
JSON.stringify(verifiedAddressList)
|
||||||
)
|
)
|
||||||
|
if (fromBlockList?.length > 0 && !c.env.KV) {
|
||||||
|
return c.text("Please enable KV to use fromBlockList", 400)
|
||||||
|
}
|
||||||
|
if (fromBlockList) {
|
||||||
|
await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || []))
|
||||||
|
}
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true
|
success: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||||
"isS3Enabled": isS3Enabled(c),
|
"isS3Enabled": isS3Enabled(c),
|
||||||
"version": CONSTANTS.VERSION,
|
"version": CONSTANTS.VERSION,
|
||||||
|
"showGithub": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VERSION: 'v0.6.1',
|
VERSION: 'v0.7.1',
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
@@ -13,4 +13,5 @@ export const CONSTANTS = {
|
|||||||
TG_KV_SETTINGS_KEY: "temp-mail-telegram-settings",
|
TG_KV_SETTINGS_KEY: "temp-mail-telegram-settings",
|
||||||
WEBHOOK_KV_SETTINGS_KEY: "temp-mail-webhook-settings",
|
WEBHOOK_KV_SETTINGS_KEY: "temp-mail-webhook-settings",
|
||||||
WEBHOOK_KV_USER_SETTINGS_KEY: "temp-mail-webhook-user-settings",
|
WEBHOOK_KV_USER_SETTINGS_KEY: "temp-mail-webhook-user-settings",
|
||||||
|
EMAIL_KV_BLACK_LIST: "temp-mail-email-black-list",
|
||||||
}
|
}
|
||||||
|
|||||||
16
worker/src/email/black_list.ts
Normal file
16
worker/src/email/black_list.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CONSTANTS } from "../constants";
|
||||||
|
import { Bindings } from "../types";
|
||||||
|
|
||||||
|
export const isBlocked = async (from: string, env: Bindings): Promise<boolean> => {
|
||||||
|
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!env.KV) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const blockList = await env.KV.get<string[]>(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') || [];
|
||||||
|
if (blockList.some(word => from.includes(word))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -5,11 +5,12 @@ import { sendMailToTelegram } from "../telegram_api";
|
|||||||
import { Bindings, HonoCustomType } from "../types";
|
import { Bindings, HonoCustomType } from "../types";
|
||||||
import { auto_reply } from "./auto_reply";
|
import { auto_reply } from "./auto_reply";
|
||||||
import { trigerWebhook } from "../mails_api/webhook_settings";
|
import { trigerWebhook } from "../mails_api/webhook_settings";
|
||||||
|
import { isBlocked } from "./black_list";
|
||||||
|
|
||||||
|
|
||||||
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
|
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
|
||||||
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => message.from.includes(word))) {
|
if (await isBlocked(message.from, env)) {
|
||||||
message.setReject("Missing from address");
|
message.setReject("Reject from address");
|
||||||
console.log(`Reject message from ${message.from} to ${message.to}`);
|
console.log(`Reject message from ${message.from} to ${message.to}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
import { HonoCustomType } from "../types";
|
import { HonoCustomType } from "../types";
|
||||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue } from '../utils';
|
||||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import auto_reply from './auto_reply'
|
import auto_reply from './auto_reply'
|
||||||
@@ -49,6 +49,7 @@ api.delete('/api/mails/:id', async (c) => {
|
|||||||
|
|
||||||
api.get('/api/settings', async (c) => {
|
api.get('/api/settings', async (c) => {
|
||||||
const { address, address_id } = c.get("jwtPayload")
|
const { address, address_id } = c.get("jwtPayload")
|
||||||
|
const user_role = c.get("userRolePayload")
|
||||||
if (address_id && address_id > 0) {
|
if (address_id && address_id > 0) {
|
||||||
try {
|
try {
|
||||||
const db_address_id = await c.env.DB.prepare(
|
const db_address_id = await c.env.DB.prepare(
|
||||||
@@ -82,7 +83,8 @@ api.get('/api/settings', async (c) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to update address")
|
console.warn("Failed to update address")
|
||||||
}
|
}
|
||||||
const balance = await c.env.DB.prepare(
|
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||||
|
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
||||||
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
||||||
).bind(address).first("balance");
|
).bind(address).first("balance");
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createMimeMessage } from 'mimetext';
|
|||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue } from '../utils';
|
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue } from '../utils';
|
||||||
import { GeoData } from '../models'
|
import { GeoData } from '../models'
|
||||||
import { handleListQuery } from '../common'
|
import { handleListQuery } from '../common'
|
||||||
import { HonoCustomType } from '../types';
|
import { HonoCustomType } from '../types';
|
||||||
@@ -105,13 +105,17 @@ export const sendMail = async (
|
|||||||
if (!domains.includes(mailDomain)) {
|
if (!domains.includes(mailDomain)) {
|
||||||
throw new Error("Invalid domain")
|
throw new Error("Invalid domain")
|
||||||
}
|
}
|
||||||
// check permission
|
const user_role = c.get("userRolePayload");
|
||||||
const balance = await c.env.DB.prepare(
|
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||||
`SELECT balance FROM address_sender
|
if (!is_no_limit_send_balance) {
|
||||||
|
// check permission
|
||||||
|
const balance = await c.env.DB.prepare(
|
||||||
|
`SELECT balance FROM address_sender
|
||||||
where address = ? and enabled = 1`
|
where address = ? and enabled = 1`
|
||||||
).bind(address).first<number>("balance");
|
).bind(address).first<number>("balance");
|
||||||
if (!balance || balance <= 0) {
|
if (!balance || balance <= 0) {
|
||||||
throw new Error("No balance")
|
throw new Error("No balance")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
from_name, to_mail, to_name,
|
from_name, to_mail, to_name,
|
||||||
@@ -154,7 +158,7 @@ export const sendMail = async (
|
|||||||
throw new Error("Please enable resend or verified address list")
|
throw new Error("Please enable resend or verified address list")
|
||||||
}
|
}
|
||||||
// update balance
|
// update balance
|
||||||
if (!sendByVerifiedAddressList) {
|
if (!sendByVerifiedAddressList && !is_no_limit_send_balance) {
|
||||||
try {
|
try {
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`UPDATE address_sender SET balance = balance - 1 where address = ?`
|
`UPDATE address_sender SET balance = balance - 1 where address = ?`
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import type {
|
||||||
|
AuthenticatorTransportFuture,
|
||||||
|
CredentialDeviceType,
|
||||||
|
Base64URLString,
|
||||||
|
} from '@simplewebauthn/types';
|
||||||
|
|
||||||
|
export type Passkey = {
|
||||||
|
id: Base64URLString;
|
||||||
|
publicKey: string;
|
||||||
|
counter: number;
|
||||||
|
deviceType: CredentialDeviceType;
|
||||||
|
backedUp: boolean;
|
||||||
|
transports?: AuthenticatorTransportFuture[];
|
||||||
|
};
|
||||||
|
|
||||||
export class AdminWebhookSettings {
|
export class AdminWebhookSettings {
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
|
|
||||||
|
|||||||
7
worker/src/types.d.ts
vendored
7
worker/src/types.d.ts
vendored
@@ -19,11 +19,13 @@ export type Bindings = {
|
|||||||
MAX_ADDRESS_LEN: string | number | undefined
|
MAX_ADDRESS_LEN: string | number | undefined
|
||||||
DEFAULT_DOMAINS: string | string[] | undefined
|
DEFAULT_DOMAINS: string | string[] | undefined
|
||||||
DOMAINS: string | string[] | undefined
|
DOMAINS: string | string[] | undefined
|
||||||
|
ADMIN_USER_ROLE: string | undefined
|
||||||
USER_DEFAULT_ROLE: string | UserRole | undefined
|
USER_DEFAULT_ROLE: string | UserRole | undefined
|
||||||
USER_ROLES: string | UserRole[] | undefined
|
USER_ROLES: string | UserRole[] | undefined
|
||||||
DOMAIN_LABELS: string | string[] | undefined
|
DOMAIN_LABELS: string | string[] | undefined
|
||||||
PASSWORDS: string | string[] | undefined
|
PASSWORDS: string | string[] | undefined
|
||||||
ADMIN_PASSWORDS: string | string[] | undefined
|
ADMIN_PASSWORDS: string | string[] | undefined
|
||||||
|
DISABLE_ADMIN_PASSWORD_CHECK: string | boolean | undefined
|
||||||
JWT_SECRET: string
|
JWT_SECRET: string
|
||||||
BLACK_LIST: string | undefined
|
BLACK_LIST: string | undefined
|
||||||
ENABLE_AUTO_REPLY: string | boolean | undefined
|
ENABLE_AUTO_REPLY: string | boolean | undefined
|
||||||
@@ -32,8 +34,10 @@ export type Bindings = {
|
|||||||
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
ENABLE_USER_DELETE_EMAIL: string | boolean | 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
|
||||||
ADMIN_CONTACT: string | undefined
|
ADMIN_CONTACT: string | undefined
|
||||||
COPYRIGHT: string | undefined
|
COPYRIGHT: string | undefined
|
||||||
|
DISABLE_SHOW_GITHUB: string | boolean | undefined
|
||||||
FORWARD_ADDRESS_LIST: string | string[] | undefined
|
FORWARD_ADDRESS_LIST: string | string[] | undefined
|
||||||
|
|
||||||
// s3 config
|
// s3 config
|
||||||
@@ -70,7 +74,8 @@ type UserPayload = {
|
|||||||
|
|
||||||
type Variables = {
|
type Variables = {
|
||||||
userPayload: UserPayload,
|
userPayload: UserPayload,
|
||||||
jwtPayload: JwtPayload
|
userRolePayload: string | undefined | null,
|
||||||
|
jwtPayload: JwtPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
type HonoCustomType = {
|
type HonoCustomType = {
|
||||||
|
|||||||
@@ -4,15 +4,30 @@ import { HonoCustomType } from '../types';
|
|||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import bind_address from './bind_address';
|
import bind_address from './bind_address';
|
||||||
|
import passkey from './passkey';
|
||||||
|
|
||||||
export const api = new Hono<HonoCustomType>();
|
export const api = new Hono<HonoCustomType>();
|
||||||
|
|
||||||
|
// settings api
|
||||||
api.get('/user_api/open_settings', settings.openSettings);
|
api.get('/user_api/open_settings', settings.openSettings);
|
||||||
api.get('/user_api/settings', settings.settings);
|
api.get('/user_api/settings', settings.settings);
|
||||||
|
|
||||||
|
// user api
|
||||||
api.post('/user_api/login', user.login);
|
api.post('/user_api/login', user.login);
|
||||||
api.post('/user_api/verify_code', user.verifyCode);
|
api.post('/user_api/verify_code', user.verifyCode);
|
||||||
api.post('/user_api/register', user.register);
|
api.post('/user_api/register', user.register);
|
||||||
|
|
||||||
|
// bind address api
|
||||||
api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
||||||
api.post('/user_api/bind_address', bind_address.bind);
|
api.post('/user_api/bind_address', bind_address.bind);
|
||||||
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
||||||
api.post('/user_api/unbind_address', bind_address.unbind);
|
api.post('/user_api/unbind_address', bind_address.unbind);
|
||||||
|
|
||||||
|
// passkey api
|
||||||
|
api.get('/user_api/passkey', passkey.getPassKeys);
|
||||||
|
api.post('/user_api/passkey/rename', passkey.renamePassKey);
|
||||||
|
api.delete('/user_api/passkey/:passkey_id', passkey.deletePassKey);
|
||||||
|
api.post('/user_api/passkey/register_request', passkey.registerRequest);
|
||||||
|
api.post('/user_api/passkey/register_response', passkey.registerResponse);
|
||||||
|
api.post('/user_api/passkey/authenticate_request', passkey.authenticateRequest);
|
||||||
|
api.post('/user_api/passkey/authenticate_response', passkey.authenticateResponse);
|
||||||
|
|||||||
204
worker/src/user_api/passkey.ts
Normal file
204
worker/src/user_api/passkey.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { Context } from 'hono';
|
||||||
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
import {
|
||||||
|
generateRegistrationOptions,
|
||||||
|
verifyRegistrationResponse,
|
||||||
|
generateAuthenticationOptions,
|
||||||
|
verifyAuthenticationResponse
|
||||||
|
} from '@simplewebauthn/server';
|
||||||
|
|
||||||
|
import { HonoCustomType } from '../types';
|
||||||
|
import { Passkey } from '../models';
|
||||||
|
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
||||||
|
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getPassKeys: async (c: Context<HonoCustomType>) => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
const { results } = await c.env.DB.prepare(
|
||||||
|
`SELECT passkey_name, passkey_id, created_at, updated_at FROM user_passkeys WHERE user_id = ?`
|
||||||
|
).bind(user.user_id).all<Record<string, string>>();
|
||||||
|
return c.json(results);
|
||||||
|
},
|
||||||
|
renamePassKey: async (c: Context<HonoCustomType>) => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
const { passkey_id, passkey_name } = await c.req.json();
|
||||||
|
if (!passkey_name || passkey_name.length > 255) {
|
||||||
|
return c.text("Invalid passkey name", 400);
|
||||||
|
}
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?`
|
||||||
|
).bind(passkey_name, user.user_id, passkey_id).run();
|
||||||
|
return c.json({ success });
|
||||||
|
},
|
||||||
|
deletePassKey: async (c: Context<HonoCustomType>) => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
const { passkey_id } = c.req.param();
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM user_passkeys WHERE user_id = ? AND passkey_id = ?`
|
||||||
|
).bind(user.user_id, passkey_id).run();
|
||||||
|
return c.json({ success });
|
||||||
|
},
|
||||||
|
registerRequest: async (c: Context<HonoCustomType>) => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
const { domain } = await c.req.json();
|
||||||
|
const { results } = await c.env.DB.prepare(
|
||||||
|
`SELECT passkey FROM user_passkeys WHERE user_id = ?`
|
||||||
|
).bind(user.user_id).all<Record<string, string>>();
|
||||||
|
const excludeCredentials = results
|
||||||
|
.map((record: any) => JSON.parse(record.passkey) as Passkey)
|
||||||
|
.map((passkey: Passkey) => ({
|
||||||
|
id: passkey.id,
|
||||||
|
transports: passkey.transports,
|
||||||
|
}));
|
||||||
|
// create challenge with 1 hour expiration
|
||||||
|
const challenge = await Jwt.sign({
|
||||||
|
user_email: user.user_email,
|
||||||
|
user_id: user.user_id,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
|
// Use SimpleWebAuthn's handy function to create registration options.
|
||||||
|
const options = await generateRegistrationOptions({
|
||||||
|
rpName: c.env.TITLE || "Temp Mail",
|
||||||
|
rpID: domain,
|
||||||
|
userID: new TextEncoder().encode(user.user_id.toString()),
|
||||||
|
userName: user.user_email,
|
||||||
|
userDisplayName: user.user_email,
|
||||||
|
attestationType: 'none',
|
||||||
|
excludeCredentials: excludeCredentials,
|
||||||
|
challenge: challenge,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(options);
|
||||||
|
},
|
||||||
|
registerResponse: async (c: Context<HonoCustomType>) => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
const { credential, origin, passkey_name } = await c.req.json();
|
||||||
|
// Verify the registration response
|
||||||
|
const verification = await verifyRegistrationResponse({
|
||||||
|
response: credential,
|
||||||
|
expectedChallenge: async (challenge: string) => {
|
||||||
|
const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256");
|
||||||
|
if (!payload || !payload.iat) return false;
|
||||||
|
// check iad is not older than 5 minutes
|
||||||
|
if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false;
|
||||||
|
if (payload.user_id !== user.user_id) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
expectedOrigin: origin,
|
||||||
|
requireUserVerification: true,
|
||||||
|
});
|
||||||
|
const { verified, registrationInfo } = verification;
|
||||||
|
|
||||||
|
if (!verified || !registrationInfo) {
|
||||||
|
return c.text("Registration failed", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
credentialID, credentialPublicKey,
|
||||||
|
counter, credentialDeviceType, credentialBackedUp,
|
||||||
|
} = registrationInfo;
|
||||||
|
|
||||||
|
// Base64URL encode ArrayBuffers.
|
||||||
|
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
|
||||||
|
|
||||||
|
const newPasskey: Passkey = {
|
||||||
|
id: credentialID,
|
||||||
|
publicKey: base64PublicKey,
|
||||||
|
counter,
|
||||||
|
deviceType: credentialDeviceType,
|
||||||
|
backedUp: credentialBackedUp,
|
||||||
|
transports: credential?.response?.transports,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the credential ID in the database
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO user_passkeys (user_id, passkey_name, passkey_id, passkey, counter) VALUES (?, ?, ?, ?, ?)`
|
||||||
|
).bind(user.user_id, passkey_name, credentialID, JSON.stringify(newPasskey), counter).run();
|
||||||
|
|
||||||
|
return c.json({ success });
|
||||||
|
},
|
||||||
|
authenticateRequest: async (c: Context<HonoCustomType>) => {
|
||||||
|
const { domain } = await c.req.json();
|
||||||
|
const challenge = await Jwt.sign({
|
||||||
|
domain,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
|
const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({
|
||||||
|
rpID: domain,
|
||||||
|
challenge: challenge,
|
||||||
|
allowCredentials: [],
|
||||||
|
});
|
||||||
|
return c.json(options);
|
||||||
|
},
|
||||||
|
authenticateResponse: async (c: Context<HonoCustomType>) => {
|
||||||
|
const { domain, credential, origin } = await c.req.json();
|
||||||
|
const passkey_id = credential?.id;
|
||||||
|
if (!passkey_id) {
|
||||||
|
return c.text("Invalid request", 400);
|
||||||
|
}
|
||||||
|
const { user_id, counter, passkey } = await c.env.DB.prepare(
|
||||||
|
`SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?`
|
||||||
|
).bind(passkey_id).first<{
|
||||||
|
counter: number; passkey: string; user_id: number;
|
||||||
|
}>() || {};
|
||||||
|
if (!passkey) {
|
||||||
|
return c.text("Passkey not found", 404);
|
||||||
|
}
|
||||||
|
const passkeyData = JSON.parse(passkey) as Passkey;
|
||||||
|
// Verify the registration response
|
||||||
|
const verification = await verifyAuthenticationResponse({
|
||||||
|
response: credential,
|
||||||
|
expectedChallenge: async (challenge: string) => {
|
||||||
|
const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256");
|
||||||
|
if (!payload || !payload.iat) return false;
|
||||||
|
// check iad is not older than 5 minutes
|
||||||
|
if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
expectedOrigin: origin,
|
||||||
|
expectedRPID: domain,
|
||||||
|
authenticator: {
|
||||||
|
credentialID: passkeyData.id,
|
||||||
|
credentialPublicKey: isoBase64URL.toBuffer(passkeyData.publicKey),
|
||||||
|
counter: counter || passkeyData.counter,
|
||||||
|
transports: passkeyData.transports,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { verified, authenticationInfo } = verification;
|
||||||
|
if (!verified) {
|
||||||
|
return c.text("Authentication failed", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticationInfo) {
|
||||||
|
const { newCounter } = authenticationInfo;
|
||||||
|
// Update the counter in the database
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE user_passkeys SET counter = ? WHERE passkey_id = ?`
|
||||||
|
).bind(newCounter, passkey_id).run();
|
||||||
|
}
|
||||||
|
// update passkey updated_at
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE user_passkeys SET updated_at = datetime('now') WHERE passkey_id = ?`
|
||||||
|
).bind(passkey_id).run();
|
||||||
|
|
||||||
|
// return jwt
|
||||||
|
const { user_email } = await c.env.DB.prepare(
|
||||||
|
`SELECT user_email FROM users WHERE id = ?`
|
||||||
|
).bind(user_id).first<{ user_email: string }>() || {};
|
||||||
|
if (!user_email) {
|
||||||
|
return c.text("User not found", 404);
|
||||||
|
}
|
||||||
|
// create jwt
|
||||||
|
const jwt = await Jwt.sign({
|
||||||
|
user_email: user_email,
|
||||||
|
user_id: user_id,
|
||||||
|
// 30 days expire in seconds
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
|
return c.json({
|
||||||
|
jwt: jwt
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { UserSettings } from "../models";
|
|||||||
import { getJsonSetting, getUserRoles } from "../utils"
|
import { getJsonSetting, getUserRoles } from "../utils"
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
import { commonGetUserRole } from "../common";
|
import { commonGetUserRole } from "../common";
|
||||||
|
import { Jwt } from "hono/utils/jwt";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
openSettings: async (c: Context<HonoCustomType>) => {
|
openSettings: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -25,8 +26,23 @@ export default {
|
|||||||
return c.text("User not found", 400);
|
return c.text("User not found", 400);
|
||||||
}
|
}
|
||||||
const user_role = await commonGetUserRole(c, db_user_id);
|
const user_role = await commonGetUserRole(c, db_user_id);
|
||||||
|
const is_admin = (
|
||||||
|
c.env.ADMIN_USER_ROLE
|
||||||
|
&&
|
||||||
|
c.env.ADMIN_USER_ROLE === user_role?.role
|
||||||
|
);
|
||||||
|
const access_token = is_admin ? await Jwt.sign({
|
||||||
|
user_email: user.user_email,
|
||||||
|
user_id: user.user_id,
|
||||||
|
user_role: user_role?.role,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
// 1 hour
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||||
|
}, c.env.JWT_SECRET, "HS256") : null;
|
||||||
return c.json({
|
return c.json({
|
||||||
...user,
|
...user,
|
||||||
|
is_admin: is_admin,
|
||||||
|
access_token: access_token,
|
||||||
user_role: user_role
|
user_role: user_role
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export const getBooleanValue = (
|
|||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return value === "true";
|
return value === "true";
|
||||||
}
|
}
|
||||||
console.error(`Failed to parse boolean value: ${value}`);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,26 @@ const checkUserPayload = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkoutUserRolePayload = async (
|
||||||
|
c: Context<HonoCustomType>
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const token = c.req.raw.headers.get("x-user-access-token");
|
||||||
|
if (!token) return;
|
||||||
|
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||||
|
// check expired
|
||||||
|
if (!payload.exp) return;
|
||||||
|
// exp is in seconds
|
||||||
|
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof payload?.user_role !== "string") return;
|
||||||
|
c.set("userRolePayload", payload.user_role);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// api auth
|
// api auth
|
||||||
app.use('/api/*', async (c, next) => {
|
app.use('/api/*', async (c, next) => {
|
||||||
// check header x-custom-auth
|
// check header x-custom-auth
|
||||||
@@ -90,6 +110,11 @@ app.use('/api/*', async (c, next) => {
|
|||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (c.req.path.startsWith("/api/settings")
|
||||||
|
|| c.req.path.startsWith("/api/send_mail")
|
||||||
|
) {
|
||||||
|
await checkoutUserRolePayload(c);
|
||||||
|
}
|
||||||
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||||
});
|
});
|
||||||
// user_api auth
|
// user_api auth
|
||||||
@@ -99,6 +124,7 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
|| c.req.path.startsWith("/user_api/register")
|
|| c.req.path.startsWith("/user_api/register")
|
||||||
|| c.req.path.startsWith("/user_api/login")
|
|| c.req.path.startsWith("/user_api/login")
|
||||||
|| c.req.path.startsWith("/user_api/verify_code")
|
|| c.req.path.startsWith("/user_api/verify_code")
|
||||||
|
|| c.req.path.startsWith("/user_api/passkey/authenticate_")
|
||||||
) {
|
) {
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
@@ -127,6 +153,7 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
});
|
});
|
||||||
// admin auth
|
// admin auth
|
||||||
app.use('/admin/*', async (c, next) => {
|
app.use('/admin/*', async (c, next) => {
|
||||||
|
|
||||||
// check header x-admin-auth
|
// check header x-admin-auth
|
||||||
const adminPasswords = getAdminPasswords(c);
|
const adminPasswords = getAdminPasswords(c);
|
||||||
if (adminPasswords && adminPasswords.length > 0) {
|
if (adminPasswords && adminPasswords.length > 0) {
|
||||||
@@ -136,6 +163,33 @@ app.use('/admin/*', async (c, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// check if user is admin
|
||||||
|
const access_token = c.req.raw.headers.get("x-user-access-token");
|
||||||
|
if (c.env.ADMIN_USER_ROLE && access_token) {
|
||||||
|
try {
|
||||||
|
const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, "HS256");
|
||||||
|
// check expired
|
||||||
|
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||||
|
// exp is in seconds
|
||||||
|
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||||
|
return c.text("Token Expired", 401)
|
||||||
|
}
|
||||||
|
if (payload.user_role !== c.env.ADMIN_USER_ROLE) {
|
||||||
|
return c.text("Need Admin Role", 401)
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable admin api check
|
||||||
|
if (getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return c.text("Need Admin Password", 401)
|
return c.text("Need Admin Password", 401)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ PREFIX = "tmp"
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# For admin panel
|
# For admin panel
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# warning: no password or user check for admin portal
|
||||||
|
# DISABLE_ADMIN_PASSWORD_CHECK = false
|
||||||
# ADMIN CONTACT, CAN BE ANY STRING
|
# ADMIN CONTACT, CAN BE ANY STRING
|
||||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||||
@@ -33,6 +35,7 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
|
|||||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||||
|
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||||
# USER_ROLES = [
|
# USER_ROLES = [
|
||||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||||
@@ -50,13 +53,15 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# ENABLE_WEBHOOK = true
|
# ENABLE_WEBHOOK = true
|
||||||
# Footer text
|
# Footer text
|
||||||
# COPYRIGHT = "Dream Hunter"
|
# COPYRIGHT = "Dream Hunter"
|
||||||
|
# DISABLE_SHOW_GITHUB = true
|
||||||
# default send balance, if not set, it will be 0
|
# default send balance, if not set, it will be 0
|
||||||
# DEFAULT_SEND_BALANCE = 1
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
|
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||||
# Turnstile verification
|
# Turnstile verification
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# telegram bot
|
# telegram bot
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ADDRESS = 5
|
||||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user