mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-06 07:57:04 +08:00
feat: add USER_ROLES && admin pages search when keybord enter && auto trim (#348)
* feat: add USER_ROLES * feat: admin pages search when keybord enter && auto trim * feat: update version to v0.6.0
This commit is contained in:
+12
-1
@@ -1,10 +1,21 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
## main branch
|
## v0.6.0
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
DB changes: 增加用户角色表, 需要执行 `db/2024-07-14-patch.sql` 更新 `D1` 数据库
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
worker 配置文件新增 `DEFAULT_DOMAINS`, `USER_ROLES`, 具体查看文档 [worker配置](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html#%E4%BF%AE%E6%94%B9-wrangler-toml-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)
|
||||||
|
|
||||||
- 移除 `apiV1` 相关代码和相关的数据库表
|
- 移除 `apiV1` 相关代码和相关的数据库表
|
||||||
- 更新 `admin/statistics` api, 添加用户统计信息
|
- 更新 `admin/statistics` api, 添加用户统计信息
|
||||||
|
- 更新地址的规则,只允许小写+数字,对于历史的地址在查询邮件时会进行 `lowercase` 处理
|
||||||
|
- 增加用户角色功能,`admin` 可以设置用户角色(目前可配置每个角色域名和前缀)
|
||||||
|
- admin 页面搜索优化, 回车自动搜索, 输入内容自动 trim
|
||||||
|
|
||||||
## v0.5.4
|
## v0.5.4
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER UNIQUE NOT NULL,
|
||||||
|
role_text TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||||
@@ -78,3 +78,13 @@ CREATE TABLE IF NOT EXISTS users_address (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER UNIQUE NOT NULL,
|
||||||
|
role_text TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const getOpenSettings = async (message) => {
|
|||||||
minAddressLen: res["minAddressLen"] || 1,
|
minAddressLen: res["minAddressLen"] || 1,
|
||||||
maxAddressLen: res["maxAddressLen"] || 30,
|
maxAddressLen: res["maxAddressLen"] || 30,
|
||||||
needAuth: res["needAuth"] || false,
|
needAuth: res["needAuth"] || false,
|
||||||
|
defaultDomains: res["defaultDomains"] || [],
|
||||||
domains: res["domains"].map((domain, index) => {
|
domains: res["domains"].map((domain, index) => {
|
||||||
return {
|
return {
|
||||||
label: domainLabels.length > index ? domainLabels[index] : domain,
|
label: domainLabels.length > index ? domainLabels[index] : domain,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
enableUserDeleteEmail: false,
|
enableUserDeleteEmail: false,
|
||||||
enableAutoReply: false,
|
enableAutoReply: false,
|
||||||
enableIndexAbout: false,
|
enableIndexAbout: false,
|
||||||
|
/** @type {string[]} */
|
||||||
|
defaultDomains: [],
|
||||||
/** @type {Array<{label: string, value: string}>} */
|
/** @type {Array<{label: string, value: string}>} */
|
||||||
domains: [],
|
domains: [],
|
||||||
copyright: 'Dream Hunter',
|
copyright: 'Dream Hunter',
|
||||||
@@ -70,6 +72,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
user_email: '',
|
user_email: '',
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
|
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||||
|
user_role: null,
|
||||||
});
|
});
|
||||||
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);
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const deleteEmail = async () => {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
|
addressQuery.value = addressQuery.value.trim()
|
||||||
const { results, count: addressCount } = await api.fetch(
|
const { results, count: addressCount } = await api.fetch(
|
||||||
`/admin/address`
|
`/admin/address`
|
||||||
+ `?limit=${pageSize.value}`
|
+ `?limit=${pageSize.value}`
|
||||||
@@ -283,7 +284,8 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||||
|
@keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -29,12 +29,9 @@ const { t } = useI18n({
|
|||||||
const mailBoxKey = ref("")
|
const mailBoxKey = ref("")
|
||||||
const mailKeyword = ref("")
|
const mailKeyword = ref("")
|
||||||
|
|
||||||
watch([adminMailTabAddress, mailKeyword], () => {
|
const queryMail = () => {
|
||||||
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
||||||
mailKeyword.value = mailKeyword.value.trim();
|
mailKeyword.value = mailKeyword.value.trim();
|
||||||
});
|
|
||||||
|
|
||||||
const queryMail = () => {
|
|
||||||
mailBoxKey.value = Date.now();
|
mailBoxKey.value = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +60,9 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
|
||||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
|
@keydown.enter="queryMail" />
|
||||||
|
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||||
<n-button @click="queryMail" type="primary" tertiary>
|
<n-button @click="queryMail" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = async (limit, offset) => {
|
const fetchData = async (limit, offset) => {
|
||||||
|
adminSendBoxTabAddress.value = adminSendBoxTabAddress.value.trim();
|
||||||
return await api.fetch(
|
return await api.fetch(
|
||||||
`/admin/sendbox?limit=${limit}&offset=${offset}`
|
`/admin/sendbox?limit=${limit}&offset=${offset}`
|
||||||
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
||||||
@@ -35,7 +36,7 @@ const deleteSenboxMail = async (curMailId) => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" />
|
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" @keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const updateData = async () => {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
|
addressQuery.value = addressQuery.value.trim();
|
||||||
const { results, count: addressCount } = await api.fetch(
|
const { results, count: addressCount } = await api.fetch(
|
||||||
`/admin/address_sender`
|
`/admin/address_sender`
|
||||||
+ `?limit=${pageSize.value}`
|
+ `?limit=${pageSize.value}`
|
||||||
@@ -192,7 +193,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="addressQuery" />
|
<n-input v-model:value="addressQuery" @keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { NMenu, NButton, NBadge } from 'naive-ui';
|
import { NMenu, NButton, NBadge, NTag } from 'naive-ui';
|
||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
|
|
||||||
const { loading } = useGlobalState()
|
const { loading, openSettings } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -16,6 +16,7 @@ const { t } = useI18n({
|
|||||||
en: {
|
en: {
|
||||||
success: 'Success',
|
success: 'Success',
|
||||||
user_email: 'User Email',
|
user_email: 'User Email',
|
||||||
|
role: 'Role',
|
||||||
address_count: 'Address Count',
|
address_count: 'Address Count',
|
||||||
created_at: 'Created At',
|
created_at: 'Created At',
|
||||||
actions: 'Actions',
|
actions: 'Actions',
|
||||||
@@ -29,10 +30,15 @@ const { t } = useI18n({
|
|||||||
createUser: 'Create User',
|
createUser: 'Create User',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
|
changeRole: 'Change Role',
|
||||||
|
prefix: 'Prefix',
|
||||||
|
domains: 'Domains',
|
||||||
|
roleDonotExist: 'Current Role does not exist',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
user_email: '用户邮箱',
|
user_email: '用户邮箱',
|
||||||
|
role: '角色',
|
||||||
address_count: '地址数量',
|
address_count: '地址数量',
|
||||||
created_at: '创建时间',
|
created_at: '创建时间',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
@@ -46,6 +52,10 @@ const { t } = useI18n({
|
|||||||
createUser: '创建用户',
|
createUser: '创建用户',
|
||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
|
changeRole: '更改角色',
|
||||||
|
prefix: '前缀',
|
||||||
|
domains: '域名',
|
||||||
|
roleDonotExist: '当前角色不存在',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -64,9 +74,31 @@ const user = ref({
|
|||||||
email: "",
|
email: "",
|
||||||
password: ""
|
password: ""
|
||||||
})
|
})
|
||||||
|
const showChangeRole = ref(false)
|
||||||
|
const userRoles = ref([])
|
||||||
|
const curUserRole = ref('')
|
||||||
|
const userRolesOptions = computed(() => {
|
||||||
|
return userRoles.value.map(role => {
|
||||||
|
return {
|
||||||
|
label: role.role,
|
||||||
|
value: role.role
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchUserRoles = async () => {
|
||||||
|
try {
|
||||||
|
const results = await api.fetch(`/admin/user_roles`);
|
||||||
|
userRoles.value = results;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
|
userQuery.value = userQuery.value.trim()
|
||||||
const { results, count: userCount } = await api.fetch(
|
const { results, count: userCount } = await api.fetch(
|
||||||
`/admin/users`
|
`/admin/users`
|
||||||
+ `?limit=${pageSize.value}`
|
+ `?limit=${pageSize.value}`
|
||||||
@@ -138,6 +170,24 @@ const deleteUser = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeRole = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/user_roles`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: curUserId.value,
|
||||||
|
role_text: curUserRole.value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
message.success(t('success'));
|
||||||
|
showChangeRole.value = false;
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: "ID",
|
title: "ID",
|
||||||
@@ -147,6 +197,19 @@ const columns = [
|
|||||||
title: t('user_email'),
|
title: t('user_email'),
|
||||||
key: "user_email"
|
key: "user_email"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('role'),
|
||||||
|
key: "role_text",
|
||||||
|
render(row) {
|
||||||
|
if (!row.role_text) return null;
|
||||||
|
return h(NTag, {
|
||||||
|
bordered: false,
|
||||||
|
type: "info"
|
||||||
|
}, {
|
||||||
|
default: () => row.role_text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('address_count'),
|
title: t('address_count'),
|
||||||
key: "address_count",
|
key: "address_count",
|
||||||
@@ -176,6 +239,19 @@ const columns = [
|
|||||||
icon: () => h(MenuFilled),
|
icon: () => h(MenuFilled),
|
||||||
key: "action",
|
key: "action",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
curUserId.value = row.id;
|
||||||
|
curUserRole.value = row.role_text;
|
||||||
|
showChangeRole.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('changeRole') }
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
{
|
{
|
||||||
@@ -212,12 +288,29 @@ const columns = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const getRolePrefix = (role) => {
|
||||||
|
const res = userRoles.value.find(r => r.role === role)?.prefix;
|
||||||
|
if (res === undefined || res === null) return openSettings.value.prefix;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoleDomains = (role) => {
|
||||||
|
const res = userRoles.value.find(r => r.role === role)?.domains;
|
||||||
|
if (res === undefined || res === null || res.length == 0) return openSettings.value.defaultDomains;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleDonotExist = computed(() => {
|
||||||
|
return !userRoles.value.some(r => r.role === curUserRole.value);
|
||||||
|
})
|
||||||
|
|
||||||
watch([page, pageSize], async () => {
|
watch([page, pageSize], async () => {
|
||||||
await fetchData()
|
await fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchData()
|
await fetchUserRoles();
|
||||||
|
await fetchData();
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -256,8 +349,21 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showChangeRole" preset="dialog" :title="t('changeRole')">
|
||||||
|
<n-alert type="error" :bordered="false" v-if="roleDonotExist">
|
||||||
|
<span>{{ t('roleDonotExist') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<p>{{ t('prefix') + ": " + getRolePrefix(curUserRole) }}</p>
|
||||||
|
<p>{{ t('domains') + ": " + JSON.stringify(getRoleDomains(curUserRole)) }}</p>
|
||||||
|
<n-select clearable v-model:value="curUserRole" :options="userRolesOptions" />
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="changeRole" size="small" tertiary type="primary">
|
||||||
|
{{ t('changeRole') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="userQuery" />
|
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
||||||
@@ -140,11 +140,39 @@ const newEmail = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addressPrefix = computed(() => {
|
||||||
|
// if user has role, return role prefix
|
||||||
|
if (userSettings.value?.user_role) {
|
||||||
|
return userSettings.value.user_role.prefix || "";
|
||||||
|
}
|
||||||
|
// if user has no role, return default prefix
|
||||||
|
return openSettings.value.prefix;
|
||||||
|
});
|
||||||
|
|
||||||
|
const domainsOptions = computed(() => {
|
||||||
|
// if user has role, return role domains
|
||||||
|
if (userSettings.value.user_role) {
|
||||||
|
const allDomains = userSettings.value.user_role.domains;
|
||||||
|
if (!allDomains) return openSettings.value.domains;
|
||||||
|
return openSettings.value.domains.filter((domain) => {
|
||||||
|
return allDomains.includes(domain.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// if user has no role, return default domains
|
||||||
|
if (!openSettings.value.defaultDomains) {
|
||||||
|
return openSettings.value.domains;
|
||||||
|
}
|
||||||
|
// if user has no role and no default domains, return all domains
|
||||||
|
return openSettings.value.domains.filter((domain) => {
|
||||||
|
return openSettings.value.defaultDomains.includes(domain.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
||||||
await api.getOpenSettings();
|
await api.getOpenSettings();
|
||||||
}
|
}
|
||||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
|
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -186,14 +214,14 @@ onMounted(async () => {
|
|||||||
{{ t('generateName') }}
|
{{ t('generateName') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input-group-label v-if="openSettings.prefix">
|
<n-input-group-label v-if="addressPrefix">
|
||||||
{{ openSettings.prefix }}
|
{{ addressPrefix }}
|
||||||
</n-input-group-label>
|
</n-input-group-label>
|
||||||
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
||||||
:maxlength="openSettings.maxAddressLen" />
|
:maxlength="openSettings.maxAddressLen" />
|
||||||
<n-input-group-label>@</n-input-group-label>
|
<n-input-group-label>@</n-input-group-label>
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
:options="openSettings.domains" />
|
:options="domainsOptions" />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<Turnstile v-model:value="cfToken" />
|
<Turnstile v-model:value="cfToken" />
|
||||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||||
|
|||||||
@@ -85,9 +85,15 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
|
|||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
# 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"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||||
|
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 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 = [
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" },
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" },
|
||||||
|
# ]
|
||||||
JWT_SECRET = "xxx" # Key used to generate jwt
|
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||||
# Allow users to create email addresses
|
# Allow users to create email addresses
|
||||||
|
|||||||
@@ -53,9 +53,16 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
|
|||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
# admin 联系方式,不配置则不显示,可配置任意字符串
|
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
|
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||||
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
|
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
|
||||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||||
|
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
||||||
|
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
||||||
|
# USER_ROLES = [
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" },
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" },
|
||||||
|
# ]
|
||||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||||
# 是否允许用户创建邮件, 不配置则不允许
|
# 是否允许用户创建邮件, 不配置则不允许
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
|
|
||||||
import { CONSTANTS } from '../constants';
|
import { CONSTANTS } from '../constants';
|
||||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
|
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||||
import { handleListQuery } from '../common'
|
import { handleListQuery } from '../common'
|
||||||
import { HonoCustomType } from '../types';
|
import { HonoCustomType } from '../types';
|
||||||
@@ -38,18 +38,22 @@ export default {
|
|||||||
const { limit, offset, query } = c.req.query();
|
const { limit, offset, query } = c.req.query();
|
||||||
if (query) {
|
if (query) {
|
||||||
return await handleListQuery(c,
|
return await handleListQuery(c,
|
||||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||||
|
+ ` ur.role_text as role_text,`
|
||||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||||
+ ` FROM users u`
|
+ ` FROM users u`
|
||||||
|
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`
|
||||||
+ ` where u.user_email like ?`,
|
+ ` where u.user_email like ?`,
|
||||||
`SELECT count(*) as count FROM users where user_email like ?`,
|
`SELECT count(*) as count FROM users where user_email like ?`,
|
||||||
[`%${query}%`], limit, offset
|
[`%${query}%`], limit, offset
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await handleListQuery(c,
|
return await handleListQuery(c,
|
||||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||||
|
+ ` ur.role_text as role_text,`
|
||||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||||
+ ` FROM users u`,
|
+ ` FROM users u`
|
||||||
|
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`,
|
||||||
`SELECT count(*) as count FROM users`,
|
`SELECT count(*) as count FROM users`,
|
||||||
[], limit, offset
|
[], limit, offset
|
||||||
);
|
);
|
||||||
@@ -114,4 +118,30 @@ export default {
|
|||||||
}
|
}
|
||||||
return c.json({ success: true });
|
return c.json({ success: true });
|
||||||
},
|
},
|
||||||
|
updateUserRoles: async (c: Context<HonoCustomType>) => {
|
||||||
|
const { user_id, role_text } = await c.req.json();
|
||||||
|
if (!user_id) return c.text("Invalid user_id", 400);
|
||||||
|
if (!role_text) {
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM user_roles WHERE user_id = ?`
|
||||||
|
).bind(user_id).run();
|
||||||
|
if (!success) {
|
||||||
|
return c.text("Failed to update user roles", 500)
|
||||||
|
}
|
||||||
|
return c.json({ success: true })
|
||||||
|
}
|
||||||
|
const user_roles = getUserRoles(c);
|
||||||
|
if (!user_roles.find((r) => r.role === role_text)) {
|
||||||
|
return c.text("Invalid role_text", 400)
|
||||||
|
}
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO user_roles (user_id, role_text)`
|
||||||
|
+ ` VALUES (?, ?)`
|
||||||
|
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
|
||||||
|
).bind(user_id, role_text, role_text).run();
|
||||||
|
if (!success) {
|
||||||
|
return c.text("Failed to update user roles", 500)
|
||||||
|
}
|
||||||
|
return c.json({ success: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import { HonoCustomType } from '../types'
|
import { HonoCustomType } from '../types'
|
||||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||||
import { newAddress, handleListQuery } from '../common'
|
import { newAddress, handleListQuery } from '../common'
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import cleanup_api from './cleanup_api'
|
import cleanup_api from './cleanup_api'
|
||||||
@@ -40,7 +40,7 @@ api.post('/admin/new_address', async (c) => {
|
|||||||
return c.text("Please provide a name", 400)
|
return c.text("Please provide a name", 400)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await newAddress(c, name, domain, enablePrefix, false);
|
const res = await newAddress(c, name, domain, enablePrefix, false, null, false);
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||||
@@ -292,5 +292,7 @@ api.get('/admin/users', admin_user_api.getUsers)
|
|||||||
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
||||||
api.post('/admin/users', admin_user_api.createUser)
|
api.post('/admin/users', admin_user_api.createUser)
|
||||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||||
|
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||||
|
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||||
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
|
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
|
||||||
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);
|
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray } from './utils';
|
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains } from './utils';
|
||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { HonoCustomType } from './types';
|
import { HonoCustomType } from './types';
|
||||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||||
@@ -20,6 +20,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"prefix": c.env.PREFIX,
|
"prefix": c.env.PREFIX,
|
||||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||||
|
"defaultDomains": getDefaultDomains(c),
|
||||||
"domains": getDomains(c),
|
"domains": getDomains(c),
|
||||||
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
||||||
"needAuth": needAuth,
|
"needAuth": needAuth,
|
||||||
|
|||||||
+44
-9
@@ -1,15 +1,17 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import { getBooleanValue, getDomains, getStringValue, getIntValue } from './utils';
|
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains } from './utils';
|
||||||
import { HonoCustomType } from './types';
|
import { HonoCustomType, UserRole } from './types';
|
||||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||||
|
|
||||||
export const newAddress = async (
|
export const newAddress = async (
|
||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
name: string, domain: string | undefined | null,
|
name: string, domain: string | undefined | null,
|
||||||
enablePrefix: boolean,
|
enablePrefix: boolean,
|
||||||
checkLengthByConfig: boolean = true
|
checkLengthByConfig: boolean = true,
|
||||||
|
addressPrefix: string | undefined | null = null,
|
||||||
|
checkAllowDomains: boolean = true
|
||||||
): Promise<{ address: string, jwt: string }> => {
|
): Promise<{ address: string, jwt: string }> => {
|
||||||
// remove special characters
|
// remove special characters
|
||||||
name = name.replace(/[^a-z0-9]/g, '')
|
name = name.replace(/[^a-z0-9]/g, '')
|
||||||
@@ -30,14 +32,16 @@ export const newAddress = async (
|
|||||||
if (name.length > maxAddressLength) {
|
if (name.length > maxAddressLength) {
|
||||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||||
}
|
}
|
||||||
// create address
|
// create address with prefix
|
||||||
if (enablePrefix) {
|
if (typeof addressPrefix === "string") {
|
||||||
|
name = addressPrefix + name;
|
||||||
|
} else if (enablePrefix) {
|
||||||
name = getStringValue(c.env.PREFIX) + name;
|
name = getStringValue(c.env.PREFIX) + name;
|
||||||
}
|
}
|
||||||
// check domain, generate random domain
|
// check domain
|
||||||
const domains = getDomains(c);
|
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
||||||
if (!domain || !domains.includes(domain)) {
|
if (!domain || !allowDomains.includes(domain)) {
|
||||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
throw new Error("Invalid domain")
|
||||||
}
|
}
|
||||||
// create address
|
// create address
|
||||||
name = name + "@" + domain;
|
name = name + "@" + domain;
|
||||||
@@ -217,3 +221,34 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commonGetUserRole = async (
|
||||||
|
c: Context<HonoCustomType>, user_id: number
|
||||||
|
): Promise<UserRole | undefined | null> => {
|
||||||
|
const user_roles = getUserRoles(c);
|
||||||
|
const role_text = await c.env.DB.prepare(
|
||||||
|
`SELECT role_text FROM user_roles where user_id = ?`
|
||||||
|
).bind(user_id).first<string | undefined | null>("role_text");
|
||||||
|
return role_text ? user_roles.find((r) => r.role === role_text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
if (!user) {
|
||||||
|
return c.env.PREFIX;
|
||||||
|
}
|
||||||
|
const user_role = await commonGetUserRole(c, user.user_id);
|
||||||
|
if (typeof user_role?.prefix === "string") {
|
||||||
|
return user_role.prefix;
|
||||||
|
}
|
||||||
|
return c.env.PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
|
||||||
|
const user = c.get("userPayload");
|
||||||
|
if (!user) {
|
||||||
|
return getDefaultDomains(c);
|
||||||
|
}
|
||||||
|
const user_role = await commonGetUserRole(c, user.user_id);
|
||||||
|
return user_role?.domains || getDefaultDomains(c);;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VERSION: 'v0.5.5',
|
VERSION: 'v0.6.0',
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
|||||||
|
|
||||||
import { HonoCustomType } from "../types";
|
import { HonoCustomType } from "../types";
|
||||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
||||||
import { newAddress, handleListQuery, deleteAddressWithData } 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'
|
||||||
import webhook_settings from './webhook_settings';
|
import webhook_settings from './webhook_settings';
|
||||||
@@ -118,7 +118,8 @@ api.post('/api/new_address', async (c) => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await newAddress(c, name, domain, true);
|
const addressPrefix = await getAddressPrefix(c);
|
||||||
|
const res = await newAddress(c, name, domain, true, true, addressPrefix);
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||||
|
|||||||
Vendored
+8
@@ -1,3 +1,9 @@
|
|||||||
|
export type UserRole = {
|
||||||
|
domains: string[] | undefined | null,
|
||||||
|
role: string,
|
||||||
|
prefix: string | undefined | null
|
||||||
|
}
|
||||||
|
|
||||||
export type Bindings = {
|
export type Bindings = {
|
||||||
// bindings
|
// bindings
|
||||||
DB: D1Database
|
DB: D1Database
|
||||||
@@ -10,7 +16,9 @@ export type Bindings = {
|
|||||||
PREFIX: string | undefined
|
PREFIX: string | undefined
|
||||||
MIN_ADDRESS_LEN: string | number | undefined
|
MIN_ADDRESS_LEN: string | number | undefined
|
||||||
MAX_ADDRESS_LEN: string | number | undefined
|
MAX_ADDRESS_LEN: string | number | undefined
|
||||||
|
DEFAULT_DOMAINS: string | string[] | undefined
|
||||||
DOMAINS: string | string[] | undefined
|
DOMAINS: string | string[] | 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
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { Context } from "hono";
|
|||||||
|
|
||||||
import { HonoCustomType } from "../types";
|
import { HonoCustomType } from "../types";
|
||||||
import { UserSettings } from "../models";
|
import { UserSettings } from "../models";
|
||||||
import { getJsonSetting } from "../utils"
|
import { getJsonSetting, getUserRoles } from "../utils"
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
|
import { commonGetUserRole } from "../common";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
openSettings: async (c: Context<HonoCustomType>) => {
|
openSettings: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -19,10 +20,14 @@ export default {
|
|||||||
// check if user exists
|
// check if user exists
|
||||||
const db_user_id = await c.env.DB.prepare(
|
const db_user_id = await c.env.DB.prepare(
|
||||||
`SELECT id FROM users where id = ?`
|
`SELECT id FROM users where id = ?`
|
||||||
).bind(user.user_id).first("id");
|
).bind(user.user_id).first<number | undefined | null>("id");
|
||||||
if (!db_user_id) {
|
if (!db_user_id) {
|
||||||
return c.text("User not found", 400);
|
return c.text("User not found", 400);
|
||||||
}
|
}
|
||||||
return c.json(user);
|
const user_role = await commonGetUserRole(c, db_user_id);
|
||||||
|
return c.json({
|
||||||
|
...user,
|
||||||
|
user_role: user_role
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -1,6 +1,7 @@
|
|||||||
import { Context } from "hono";
|
import { Context } from "hono";
|
||||||
import { createMimeMessage } from "mimetext";
|
import { createMimeMessage } from "mimetext";
|
||||||
import { HonoCustomType } from "./types";
|
import { HonoCustomType, UserRole } from "./types";
|
||||||
|
import { User } from "telegraf/types";
|
||||||
|
|
||||||
export const getJsonSetting = async (
|
export const getJsonSetting = async (
|
||||||
c: Context<HonoCustomType>, key: string
|
c: Context<HonoCustomType>, key: string
|
||||||
@@ -97,6 +98,12 @@ export const getStringArray = (
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDefaultDomains = (c: Context<HonoCustomType>): string[] => {
|
||||||
|
const domains = getStringArray(c.env.DEFAULT_DOMAINS);
|
||||||
|
if (domains && domains.length > 0) return domains;
|
||||||
|
return getDomains(c);
|
||||||
|
}
|
||||||
|
|
||||||
export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||||
if (!c.env.DOMAINS) {
|
if (!c.env.DOMAINS) {
|
||||||
return [];
|
return [];
|
||||||
@@ -113,6 +120,22 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
|||||||
return c.env.DOMAINS;
|
return c.env.DOMAINS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
|
||||||
|
if (!c.env.USER_ROLES) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// check if USER_ROLES is an array, if not use json.parse
|
||||||
|
if (!Array.isArray(c.env.USER_ROLES)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(c.env.USER_ROLES);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse USER_ROLES", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.env.USER_ROLES;
|
||||||
|
}
|
||||||
|
|
||||||
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
|
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||||
if (!c.env.PASSWORDS) {
|
if (!c.env.PASSWORDS) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
+29
-3
@@ -1,4 +1,4 @@
|
|||||||
import { Hono } from 'hono'
|
import { Context, Hono } from 'hono'
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
import { jwt } from 'hono/jwt'
|
import { jwt } from 'hono/jwt'
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
@@ -13,11 +13,16 @@ import { api as telegramApi } from './telegram_api'
|
|||||||
import { email } from './email';
|
import { email } from './email';
|
||||||
import { scheduled } from './scheduled';
|
import { scheduled } from './scheduled';
|
||||||
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
|
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
|
||||||
import { HonoCustomType } from './types';
|
import { HonoCustomType, UserPayload } from './types';
|
||||||
|
|
||||||
const app = new Hono<HonoCustomType>()
|
const app = new Hono<HonoCustomType>()
|
||||||
//cors
|
//cors
|
||||||
app.use('/*', cors());
|
app.use('/*', cors());
|
||||||
|
// error handler
|
||||||
|
app.onError((err, c) => {
|
||||||
|
console.error(err)
|
||||||
|
return c.text(`${err.name} ${err.message}`, 500)
|
||||||
|
})
|
||||||
// rate limit
|
// rate limit
|
||||||
app.use('/*', async (c, next) => {
|
app.use('/*', async (c, next) => {
|
||||||
if (
|
if (
|
||||||
@@ -50,6 +55,26 @@ app.use('/*', async (c, next) => {
|
|||||||
}
|
}
|
||||||
await next()
|
await next()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const checkUserPayload = async (
|
||||||
|
c: Context<HonoCustomType>
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const token = c.req.raw.headers.get("x-user-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;
|
||||||
|
}
|
||||||
|
c.set("userPayload", payload as UserPayload);
|
||||||
|
} 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
|
||||||
@@ -61,6 +86,7 @@ app.use('/api/*', async (c, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.req.path.startsWith("/api/new_address")) {
|
if (c.req.path.startsWith("/api/new_address")) {
|
||||||
|
await checkUserPayload(c);
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -87,7 +113,7 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||||
return c.text("Token Expired", 401)
|
return c.text("Token Expired", 401)
|
||||||
}
|
}
|
||||||
c.set("userPayload", payload);
|
c.set("userPayload", payload as UserPayload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return c.text("Need User Token", 401)
|
return c.text("Need User Token", 401)
|
||||||
|
|||||||
@@ -27,9 +27,15 @@ PREFIX = "tmp"
|
|||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
# ADMIN CONTACT, CAN BE ANY STRING
|
# ADMIN CONTACT, CAN BE ANY STRING
|
||||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||||
|
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 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 = [
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" },
|
||||||
|
# { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" },
|
||||||
|
# ]
|
||||||
JWT_SECRET = "xxx"
|
JWT_SECRET = "xxx"
|
||||||
BLACK_LIST = ""
|
BLACK_LIST = ""
|
||||||
# Allow users to create email addresses
|
# Allow users to create email addresses
|
||||||
|
|||||||
Reference in New Issue
Block a user