新增规则触发人机验证和加载失败提示

This commit is contained in:
eoao
2025-07-29 20:00:00 +08:00
parent d03b7ca21d
commit 3a40cfc63b
25 changed files with 632 additions and 273 deletions

View File

@@ -255,7 +255,11 @@ const en = {
backgroundUrlDesc: 'Image URL', backgroundUrlDesc: 'Image URL',
localUpload: ' Local upload', localUpload: ' Local upload',
imageLink: 'Image URL', imageLink: 'Image URL',
backgroundWarning: 'Image file size affects website load speed.' imageLinkErrorMsg: 'Invalid image URL',
backgroundWarning: 'Image file size affects website load speed.',
rulesVerify: 'Rules',
rulesVerifyTitle: 'Trigger After {count} Daily Uses per IP',
botVerifyMsg: 'Please verify that you are human',
} }
export default en export default en

View File

@@ -4,8 +4,8 @@ import zh from './zh.js'
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
messages: { messages: {
en, zh,
zh en
}, },
}); });

View File

@@ -255,6 +255,10 @@ const zh = {
backgroundUrlDesc: '在线图片链接', backgroundUrlDesc: '在线图片链接',
localUpload: '本地上传', localUpload: '本地上传',
imageLink: '图片链接', imageLink: '图片链接',
imageLinkErrorMsg: '图片链接不正确',
backgroundWarning: '图片文件大小会影响网站加载速度', backgroundWarning: '图片文件大小会影响网站加载速度',
rulesVerify: '规则',
rulesVerifyTitle: 'IP 每天使用 {count} 次后触发',
botVerifyMsg: '请完成人机验证',
} }
export default zh export default zh

View File

@@ -6,6 +6,7 @@ import {permsToRouter} from "@/perm/perm.js";
import router from "@/router"; import router from "@/router";
import {websiteConfig} from "@/request/setting.js"; import {websiteConfig} from "@/request/setting.js";
import {cvtR2Url} from "@/utils/convert.js"; import {cvtR2Url} from "@/utils/convert.js";
import i18n from "@/i18n/index.js";
export async function init() { export async function init() {
document.title = '\u200B' document.title = '\u200B'
@@ -15,11 +16,12 @@ export async function init() {
const accountStore = useAccountStore(); const accountStore = useAccountStore();
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!settingStore.lang) { if (!settingStore.lang) {
settingStore.lang = navigator.language.split('-')[0] settingStore.lang = navigator.language.split('-')[0]
} }
i18n.global.locale.value = settingStore.lang
let setting = null; let setting = null;
if (token) { if (token) {

View File

@@ -103,7 +103,10 @@
:class="verifyShow ? 'turnstile-show' : 'turnstile-hide'" :class="verifyShow ? 'turnstile-show' : 'turnstile-hide'"
:data-sitekey="settingStore.settings.siteKey" :data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess" data-callback="onTurnstileSuccess"
></div> data-error-callback="onTurnstileError"
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">人机验证模块加载失败,请刷新浏览器</span>
</div>
</el-dialog> </el-dialog>
<el-dialog v-model="setNameShow" :title="$t('changeUserName')" > <el-dialog v-model="setNameShow" :title="$t('changeUserName')" >
<div class="container"> <div class="container">
@@ -145,6 +148,7 @@ const accountName = ref(null)
const addRef = ref({}) const addRef = ref({})
let account = null let account = null
let turnstileId = null let turnstileId = null
const botJsError = ref(false)
let verifyToken = '' let verifyToken = ''
const addForm = reactive({ const addForm = reactive({
email: '', email: '',
@@ -170,11 +174,19 @@ const openSelect = () => {
mySelect.value.toggleMenu() mySelect.value.toggleMenu()
} }
window.onTurnstileError = (e) => {
console.log('人机验加载失败')
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.register-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
};
window.onTurnstileSuccess = (token) => { window.onTurnstileSuccess = (token) => {
verifyToken = token; verifyToken = token;
setTimeout(() => {
verifyShow.value = false
},1500)
}; };
function setName() { function setName() {
@@ -338,14 +350,27 @@ function submit() {
}) })
return return
} }
if (!verifyToken && settingStore.settings.addEmailVerify === 0) { if (!verifyToken && (settingStore.settings.addEmailVerify === 0 || (settingStore.settings.addEmailVerify === 2 && settingStore.settings.addVerifyOpen))) {
verifyShow.value = true if (!verifyShow.value) {
if (!turnstileId) { verifyShow.value = true
nextTick(() => { nextTick(() => {
turnstileId = window.turnstile.render('.add-email-turnstile') if (!turnstileId) {
try {
turnstileId = window.turnstile.render('.add-email-turnstile')
} catch (e) {
botJsError.value = true
console.log('人机验证js加载失败')
}
} else {
window.turnstile.reset('.add-email-turnstile')
}
})
} else if (!botJsError.value) {
ElMessage({
message: t('botVerifyMsg'),
type: "error",
plain: true
}) })
} else {
window.turnstile.reset(turnstileId)
} }
return; return;
} }
@@ -357,16 +382,24 @@ function submit() {
addForm.email = '' addForm.email = ''
accounts.push(account) accounts.push(account)
verifyToken = '' verifyToken = ''
settingStore.settings.addVerifyOpen = account.addVerifyOpen
ElMessage({ ElMessage({
message: t('addSuccessMsg'), message: t('addSuccessMsg'),
type: "success", type: "success",
plain: true plain: true
}) })
verifyShow.value = false
userStore.refreshUserInfo() userStore.refreshUserInfo()
}).catch(res => { }).catch(res => {
if (res.code === 400) { if (res.code === 400) {
verifyToken = '' verifyToken = ''
window.turnstile.reset(turnstileId) if (turnstileId) {
window.turnstile.reset(turnstileId)
} else {
nextTick(() => {
turnstileId = window.turnstile.render('.add-email-turnstile')
})
}
verifyShow.value = true verifyShow.value = true
} }
addLoading.value = false addLoading.value = false

View File

@@ -13,11 +13,12 @@
<span class="form-title">{{settingStore.settings.title}}</span> <span class="form-title">{{settingStore.settings.title}}</span>
<span class="form-desc" v-if="show === 'login'">{{$t('loginTitle')}}</span> <span class="form-desc" v-if="show === 'login'">{{$t('loginTitle')}}</span>
<span class="form-desc" v-else>{{$t('regTitle')}}</span> <span class="form-desc" v-else>{{$t('regTitle')}}</span>
<div v-if="show === 'login'"> <div v-show="show === 'login'">
<el-input class="email-input" v-model="form.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off"> <el-input class="email-input" v-model="form.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append> <template #append>
<div @click.stop="openSelect"> <div @click.stop="openSelect">
<el-select <el-select
v-if="show === 'login'"
ref="mySelect" ref="mySelect"
v-model="suffix" v-model="suffix"
:placeholder="$t('select')" :placeholder="$t('select')"
@@ -43,11 +44,12 @@
>{{$t('loginBtn')}} >{{$t('loginBtn')}}
</el-button> </el-button>
</div> </div>
<div v-else> <div v-show="show !== 'login'">
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off"> <el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append> <template #append>
<div @click.stop="openSelect"> <div @click.stop="openSelect">
<el-select <el-select
v-if="show !== 'login'"
ref="mySelect" ref="mySelect"
v-model="suffix" v-model="suffix"
:placeholder="$t('select')" :placeholder="$t('select')"
@@ -75,7 +77,12 @@
class="register-turnstile" class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey" :data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess" data-callback="onTurnstileSuccess"
></div> data-error-callback="onTurnstileError"
data-after-interactive-callback="loadAfter"
data-before-interactive-callback="loadBefore"
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">人机验证模块加载失败,请刷新浏览器</span>
</div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading" <el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>{{$t('regBtn')}} >{{$t('regBtn')}}
</el-button> </el-button>
@@ -129,15 +136,30 @@ suffix.value = domainList[0]
const verifyShow = ref(false) const verifyShow = ref(false)
let verifyToken = '' let verifyToken = ''
let turnstileId = null let turnstileId = null
let botJsError = ref(false)
window.onTurnstileSuccess = (token) => { window.onTurnstileSuccess = (token) => {
verifyToken = token; verifyToken = token;
setTimeout(() => {
verifyShow.value = false
}, 2000)
}; };
window.onTurnstileError = (e) => {
console.log('人机验加载失败')
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.register-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
};
window.loadAfter = (e) => {
console.log('loadAfter')
}
window.loadBefore = (e) => {
console.log('loadBefore')
}
const loginOpacity = computed(() => { const loginOpacity = computed(() => {
return `rgba(255, 255, 255, ${settingStore.settings.loginOpacity})` return `rgba(255, 255, 255, ${settingStore.settings.loginOpacity})`
@@ -158,7 +180,6 @@ const openSelect = () => {
mySelect.value.toggleMenu() mySelect.value.toggleMenu()
} }
const submit = () => { const submit = () => {
if (!form.email) { if (!form.email) {
@@ -267,15 +288,26 @@ function submitRegister() {
} }
if (!verifyToken && settingStore.settings.registerVerify === 0) { if (!verifyToken && (settingStore.settings.registerVerify === 0 || (settingStore.settings.registerVerify === 2 && settingStore.settings.regVerifyOpen))) {
verifyShow.value = true if (!verifyShow.value) {
if (!turnstileId) { verifyShow.value = true
nextTick(() => { nextTick(() => {
turnstileId = window.turnstile.render('.register-turnstile') if (!turnstileId) {
try {
turnstileId = window.turnstile.render('.register-turnstile')
} catch (e) {
botJsError.value = true
console.log('人机验证js加载失败')
}
} else {
window.turnstile.reset('.register-turnstile')
}
}) })
} else { } else if (!botJsError.value) {
nextTick(() => { ElMessage({
window.turnstile.reset(turnstileId); message: t('botVerifyMsg'),
type: "error",
plain: true
}) })
} }
return; return;
@@ -290,27 +322,38 @@ function submitRegister() {
code: registerForm.code code: registerForm.code
} }
register(form).then(() => { register(form).then(({regVerifyOpen}) => {
show.value = 'login' show.value = 'login'
registerForm.email = '' registerForm.email = ''
registerForm.password = '' registerForm.password = ''
registerForm.confirmPassword = '' registerForm.confirmPassword = ''
registerForm.code = '' registerForm.code = ''
registerLoading.value = false registerLoading.value = false
turnstileId = null
verifyToken = '' verifyToken = ''
settingStore.settings.regVerifyOpen = regVerifyOpen
verifyShow.value = false
ElMessage({ ElMessage({
message: t('regSuccessMsg'), message: t('regSuccessMsg'),
type: 'success', type: 'success',
plain: true, plain: true,
}) })
}).catch(res => { }).catch(res => {
registerLoading.value = false
if (res.code === 400) { if (res.code === 400) {
verifyToken = '' verifyToken = ''
window.turnstile.reset(turnstileId) settingStore.settings.regVerifyOpen = true
if (turnstileId) {
window.turnstile.reset(turnstileId)
} else {
nextTick(() => {
turnstileId = window.turnstile.render('.register-turnstile')
})
}
verifyShow.value = true verifyShow.value = true
} }
registerLoading.value = false
}); });
} }

View File

@@ -230,15 +230,39 @@
<div class="setting-item"> <div class="setting-item">
<div><span>{{$t('signUpVerification')}}</span></div> <div><span>{{$t('signUpVerification')}}</span></div>
<div> <div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <el-button class="opt-button" size="small" type="primary" @click="openRegVerifyCount">
v-model="setting.registerVerify"/> <Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
<el-select
@change="change"
:style="`width: ${ locale === 'en' ? 100 : 80 }px;`"
v-model="setting.registerVerify"
placeholder="Select"
class="bot-verify-select"
>
<el-option key="1" :value="0" :label="$t('enable')" />
<el-option key="1" :value="1" :label="$t('disable')" />
<el-option key="1" :value="2" :label="$t('rulesVerify')" />
</el-select>
</div> </div>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<div><span>{{$t('addEmailVerification')}}</span></div> <div><span>{{$t('addEmailVerification')}}</span></div>
<div> <div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <el-button class="opt-button" size="small" type="primary" @click="openAddVerifyCount">
v-model="setting.addEmailVerify"/> <Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
<el-select
@change="change"
:style="`width: ${ locale === 'en' ? 100 : 80 }px;`"
v-model="setting.addEmailVerify"
placeholder="Select"
class="bot-verify-select"
>
<el-option key="1" :value="0" :label="$t('enable')" />
<el-option key="1" :value="1" :label="$t('disable')" />
<el-option key="1" :value="2" :label="$t('rulesVerify')" />
</el-select>
</div> </div>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@@ -450,6 +474,19 @@
<el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" /> <el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" />
</el-table> </el-table>
</el-dialog> </el-dialog>
<el-dialog v-model="showRegVerifyCount" :title="$t('rulesVerifyTitle',{count: regVerifyCount})" @closed="regVerifyCount = setting.regVerifyCount" >
<form>
<el-input-number type="text" v-model="regVerifyCount" :min="1" >
</el-input-number>
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog v-model="showAddVerifyCount" :title="$t('rulesVerifyTitle',{count: addVerifyCount})" @closed="addVerifyCount = setting.addVerifyCount">
<form>
<el-input-number type="text" v-model="addVerifyCount" :min="1"/>
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{$t('save')}}</el-button>
</form>
</el-dialog>
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
@@ -497,7 +534,11 @@ const loginOpacity = ref(0)
const backgroundUrl = ref('') const backgroundUrl = ref('')
let backgroundFile = {} let backgroundFile = {}
const showSetBackground = ref(false) const showSetBackground = ref(false)
let regVerifyCount = ref(1)
let addVerifyCount = ref(1)
let backup = '{}' let backup = '{}'
const showAddVerifyCount = ref(false)
const showRegVerifyCount = ref(false)
const resendTokenForm = reactive({ const resendTokenForm = reactive({
domain: '', domain: '',
token: '', token: '',
@@ -542,8 +583,20 @@ settingQuery().then(settingData => {
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : '' backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
editTitle.value = setting.value.title editTitle.value = setting.value.title
r2DomainInput.value = setting.value.r2Domain r2DomainInput.value = setting.value.r2Domain
addVerifyCount.value = setting.value.addVerifyCount
regVerifyCount.value = setting.value.regVerifyCount
}) })
function openAddVerifyCount() {
if (settingLoading.value) return
showAddVerifyCount.value = true
}
function openRegVerifyCount() {
if (settingLoading.value) return
showRegVerifyCount.value = true
}
const resendList = computed(() => { const resendList = computed(() => {
let list = Object.keys(setting.value.resendTokens).map(key => { let list = Object.keys(setting.value.resendTokens).map(key => {
@@ -566,6 +619,20 @@ const resendList = computed(() => {
return list; return list;
}); });
function saveAddVerifyCount() {
if(!addVerifyCount.value) {
addVerifyCount.value = 1
}
editSetting({addVerifyCount: addVerifyCount.value})
}
function saveRegVerifyCount() {
if (!regVerifyCount.value) {
regVerifyCount.value = 1
}
editSetting({regVerifyCount: regVerifyCount.value})
}
const compareByLengthAndUpperCase = (a, b, key) => { const compareByLengthAndUpperCase = (a, b, key) => {
const getUpperCaseCount = (str) => (str.match(/[A-Z]/g) || []).length; const getUpperCaseCount = (str) => (str.match(/[A-Z]/g) || []).length;
if (a[key].length === b[key].length) { if (a[key].length === b[key].length) {
@@ -742,9 +809,9 @@ async function saveBackground() {
if (localUpShow.value) { if (localUpShow.value) {
image = await fileToBase64(backgroundFile,true); image = await fileToBase64(backgroundFile,true);
} else { } else {
if (!backgroundUrl.value.startsWith('http')) { if (backgroundUrl.value && !backgroundUrl.value.startsWith('http')) {
ElMessage({ ElMessage({
message: '图片地址不正确', message: t('imageLinkErrorMsg'),
type: "error", type: "error",
plain: true plain: true
}) })
@@ -872,8 +939,9 @@ function editSetting(settingForm, refreshStatus = true) {
tgSettingShow.value = false tgSettingShow.value = false
thirdEmailShow.value = false thirdEmailShow.value = false
forwardRulesShow.value = false forwardRulesShow.value = false
showAddVerifyCount.value = false
showRegVerifyCount.value = false
}).catch((e) => { }).catch((e) => {
console.error(e)
loginOpacity.value = setting.value.loginOpacity loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => { }).finally(() => {
@@ -941,7 +1009,9 @@ function editSetting(settingForm, refreshStatus = true) {
flex-direction: column; flex-direction: column;
} }
.bot-verify-select {
margin-left: 10px;
}
.settings-card { .settings-card {
background-color: #fff; background-color: #fff;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title> <title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml"> <link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BOcWPX50.js"></script> <script type="module" crossorigin src="/assets/index-BwB6muO3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CTj27R8v.css"> <link rel="stylesheet" crossorigin href="/assets/index-BEDjT-8v.css">
</head> </head>
<body> <body>
<div id="loading-first"> <div id="loading-first">

View File

@@ -1 +1,2 @@
import app from '../hono/hono'; import app from '../hono/hono';
import result from '../model/result';

View File

@@ -1,3 +1,5 @@
import verifyRecordService from '../service/verify-record-service';
export const userConst = { export const userConst = {
status: { status: {
NORMAL: 0, NORMAL: 0,
@@ -74,7 +76,7 @@ export const settingConst = {
}, },
addEmail: { addEmail: {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1
}, },
manyEmail: { manyEmail: {
OPEN: 0, OPEN: 0,
@@ -83,10 +85,12 @@ export const settingConst = {
registerVerify: { registerVerify: {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1,
COUNT: 2,
}, },
addEmailVerify: { addEmailVerify: {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1,
COUNT: 2,
}, },
forwardStatus: { forwardStatus: {
OPEN: 0, OPEN: 0,
@@ -102,6 +106,11 @@ export const settingConst = {
} }
} }
export const verifyRecordType = {
REG: 0,
ADD: 1,
}
export const isDel = { export const isDel = {
DELETE: 1, DELETE: 1,

View File

@@ -8,6 +8,8 @@ export const setting = sqliteTable('setting', {
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(), autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
addEmailVerify: integer('add_email_verify').default(1).notNull(), addEmailVerify: integer('add_email_verify').default(1).notNull(),
registerVerify: integer('register_verify').default(1).notNull(), registerVerify: integer('register_verify').default(1).notNull(),
regVerifyCount: integer('reg_verify_count').default(1).notNull(),
addVerifyCount: integer('add_verify_count').default(1).notNull(),
send: integer('send').default(1).notNull(), send: integer('send').default(1).notNull(),
r2Domain: text('r2_domain'), r2Domain: text('r2_domain'),
secretKey: text('secret_key'), secretKey: text('secret_key'),

View File

@@ -0,0 +1,10 @@
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const email = sqliteTable('verify_record', {
vrId: integer('vr_id').primaryKey({ autoIncrement: true }),
ip: integer('ip').notNull().default(''),
count: integer('count').notNull().default(1),
type: integer('type').notNull().default(0),
updateTime: text('update_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
});
export default email

View File

@@ -48,7 +48,7 @@ const en = {
noOsUpBack: 'Cannot upload background: R2 object storage not configured', noOsUpBack: 'Cannot upload background: R2 object storage not configured',
noOsDomainUpBack: 'Cannot upload background: R2 domain not configured', noOsDomainUpBack: 'Cannot upload background: R2 domain not configured',
starNotExistEmail: 'Starred email does not exist', starNotExistEmail: 'Starred email does not exist',
emptyBotToken: 'Verification token cannot be empty', emptyBotToken: 'Please verify that you are human',
botVerifyFail: 'Bot verification failed, please try again', botVerifyFail: 'Bot verification failed, please try again',
authExpired: 'Authentication has expired. Please sign in again', authExpired: 'Authentication has expired. Please sign in again',
unauthorized: 'Unauthorized', unauthorized: 'Unauthorized',

View File

@@ -48,7 +48,7 @@ const zh = {
noOsUpBack: 'r2对象存储未配置不能上传背景', noOsUpBack: 'r2对象存储未配置不能上传背景',
noOsDomainUpBack: 'r2域名未配置不能上传背景', noOsDomainUpBack: 'r2域名未配置不能上传背景',
starNotExistEmail: '星标的邮件不存在', starNotExistEmail: '星标的邮件不存在',
emptyBotToken: '验证token不能为空', emptyBotToken: '需要进行人机验证',
botVerifyFail: '人机验证失败,请重试', botVerifyFail: '人机验证失败,请重试',
authExpired: '身份认证失效,请重新登录', authExpired: '身份认证失效,请重新登录',
unauthorized: '权限不足', unauthorized: '权限不足',

View File

@@ -1,6 +1,8 @@
import app from './hono/webs'; import app from './hono/webs';
import { email } from './email/email'; import { email } from './email/email';
import userService from './service/user-service'; import userService from './service/user-service';
import verifyRecord from './entity/verify-record';
import verifyRecordService from './service/verify-record-service';
export default { export default {
async fetch(req, env, ctx) { async fetch(req, env, ctx) {
const url = new URL(req.url) const url = new URL(req.url)
@@ -16,6 +18,7 @@ export default {
}, },
email: email, email: email,
async scheduled(c, env, ctx) { async scheduled(c, env, ctx) {
await verifyRecordService.clearRecord({env})
await userService.resetDaySendCount({ env }) await userService.resetDaySendCount({ env })
}, },
}; };

View File

@@ -18,10 +18,34 @@ const init = {
await this.v1_3_1DB(c); await this.v1_3_1DB(c);
await this.v1_4DB(c); await this.v1_4DB(c);
await this.v1_5DB(c); await this.v1_5DB(c);
await this.v1_6DB(c);
await settingService.refresh(c); await settingService.refresh(c);
return c.text(t('initSuccess')); return c.text(t('initSuccess'));
}, },
async v1_6DB(c) {
const ADD_COLUMN_SQL_LIST = [
`ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN add_verify_count INTEGER NOT NULL DEFAULT 1;`,
`CREATE TABLE IF NOT EXISTS verify_record (
vr_id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT NOT NULL DEFAULT '',
count INTEGER NOT NULL DEFAULT 1,
type INTEGER NOT NULL DEFAULT 0,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
)`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
},
async v1_5DB(c) { async v1_5DB(c) {
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:list' WHERE perm_key = 'all-email:list'`).run(); await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:list' WHERE perm_key = 'all-email:list'`).run();
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:delete' WHERE perm_key = 'all-email:delete'`).run(); await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:delete' WHERE perm_key = 'all-email:delete'`).run();

View File

@@ -6,21 +6,26 @@ import emailService from './email-service';
import orm from '../entity/orm'; import orm from '../entity/orm';
import account from '../entity/account'; import account from '../entity/account';
import { and, asc, eq, gt, inArray, count, sql } from 'drizzle-orm'; import { and, asc, eq, gt, inArray, count, sql } from 'drizzle-orm';
import { isDel } from '../const/entity-const'; import { isDel, settingConst } from '../const/entity-const';
import settingService from './setting-service'; import settingService from './setting-service';
import turnstileService from './turnstile-service'; import turnstileService from './turnstile-service';
import roleService from './role-service'; import roleService from './role-service';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import verifyRecordService from './verify-record-service';
const accountService = { const accountService = {
async add(c, params, userId) { async add(c, params, userId) {
if (!await settingService.isAddEmail(c)) { const {addEmailVerify , addEmail, manyEmail, addVerifyCount} = await settingService.query(c);
let { email, token } = params;
if (!(addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN)) {
throw new BizError(t('addAccountDisabled')); throw new BizError(t('addAccountDisabled'));
} }
let { email, token } = params;
if (!email) { if (!email) {
throw new BizError(t('emptyEmail')); throw new BizError(t('emptyEmail'));
@@ -35,7 +40,7 @@ const accountService = {
} }
const accountRow = await this.selectByEmailIncludeDelNoCase(c, email); let accountRow = await this.selectByEmailIncludeDelNoCase(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) { if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelAccount')); throw new BizError(t('isDelAccount'));
@@ -61,11 +66,30 @@ const accountService = {
} }
if (await settingService.isAddEmailVerify(c)) { let addVerifyOpen = false
if (addEmailVerify === settingConst.addEmailVerify.OPEN) {
addVerifyOpen = true
await turnstileService.verify(c, token); await turnstileService.verify(c, token);
} }
return orm(c).insert(account).values({ email: email, userId: userId, name: emailUtils.getName(email) }).returning().get(); if (addEmailVerify === settingConst.addEmailVerify.COUNT) {
addVerifyOpen = await verifyRecordService.isOpenAddVerify(c, addVerifyCount);
if (addVerifyOpen) {
await turnstileService.verify(c,token)
}
}
accountRow = await orm(c).insert(account).values({ email: email, userId: userId, name: emailUtils.getName(email) }).returning().get();
if (addEmailVerify === settingConst.addEmailVerify.COUNT && !addVerifyOpen) {
const row = await verifyRecordService.increaseAddCount(c);
addVerifyOpen = row.count >= addVerifyCount
}
accountRow.addVerifyOpen = addVerifyOpen
return accountRow;
}, },
selectByEmailIncludeDelNoCase(c, email) { selectByEmailIncludeDelNoCase(c, email) {

View File

@@ -18,6 +18,7 @@ import regKeyService from './reg-key-service';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { toUtc } from '../utils/date-uitil'; import { toUtc } from '../utils/date-uitil';
import { t } from '../i18n/i18n.js'; import { t } from '../i18n/i18n.js';
import verifyRecordService from './verify-record-service';
const loginService = { const loginService = {
@@ -25,11 +26,8 @@ const loginService = {
const { email, password, token, code } = params; const { email, password, token, code } = params;
const {regKey, register, registerVerify} = await settingService.query(c) const {regKey, register, registerVerify, regVerifyCount} = await settingService.query(c)
if (registerVerify === settingConst.registerVerify.OPEN) {
await turnstileService.verify(c,token)
}
if (register === settingConst.register.CLOSE) { if (register === settingConst.register.CLOSE) {
throw new BizError(t('regDisabled')); throw new BizError(t('regDisabled'));
@@ -80,7 +78,6 @@ const loginService = {
throw new BizError(t('isRegAccount')); throw new BizError(t('isRegAccount'));
} }
const { salt, hash } = await saltHashUtils.hashPassword(password);
let defType = null let defType = null
@@ -104,6 +101,22 @@ const loginService = {
} }
let regVerifyOpen = false
if (registerVerify === settingConst.registerVerify.OPEN) {
regVerifyOpen = true
await turnstileService.verify(c,token)
}
if (registerVerify === settingConst.registerVerify.COUNT) {
regVerifyOpen = await verifyRecordService.isOpenRegVerify(c, regVerifyCount);
if (regVerifyOpen) {
await turnstileService.verify(c,token)
}
}
const { salt, hash } = await saltHashUtils.hashPassword(password);
const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType }); const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType });
await userService.updateUserInfo(c, userId, true); await userService.updateUserInfo(c, userId, true);
@@ -114,6 +127,13 @@ const loginService = {
await regKeyService.reduceCount(c, code, 1); await regKeyService.reduceCount(c, code, 1);
} }
if (registerVerify === settingConst.registerVerify.COUNT && !regVerifyOpen) {
const row = await verifyRecordService.increaseRegCount(c);
return {regVerifyOpen: row.count >= regVerifyCount}
}
return {regVerifyOpen}
}, },
async handleOpenRegKey(c, regKey, code) { async handleOpenRegKey(c, regKey, code) {

View File

@@ -1,7 +1,7 @@
import KvConst from '../const/kv-const'; import KvConst from '../const/kv-const';
import setting from '../entity/setting'; import setting from '../entity/setting';
import orm from '../entity/orm'; import orm from '../entity/orm';
import { settingConst } from '../const/entity-const'; import { settingConst, verifyRecordType } from '../const/entity-const';
import fileUtils from '../utils/file-utils'; import fileUtils from '../utils/file-utils';
import r2Service from './r2-service'; import r2Service from './r2-service';
import emailService from './email-service'; import emailService from './email-service';
@@ -10,6 +10,7 @@ import userService from './user-service';
import constant from '../const/constant'; import constant from '../const/constant';
import BizError from '../error/biz-error'; import BizError from '../error/biz-error';
import { t } from '../i18n/i18n' import { t } from '../i18n/i18n'
import verifyRecordService from './verify-record-service';
const settingService = { const settingService = {
@@ -31,11 +32,32 @@ const settingService = {
}, },
async get(c) { async get(c) {
const settingRow = await this.query(c);
const [settingRow, recordList] = await Promise.all([
await this.query(c),
verifyRecordService.selectListByIP(c)
]);
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null; settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null;
Object.keys(settingRow.resendTokens).forEach(key => { Object.keys(settingRow.resendTokens).forEach(key => {
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`; settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
}); });
let regVerifyOpen = false
let addVerifyOpen = false
recordList.forEach(row => {
if (row.type === verifyRecordType.REG) {
regVerifyOpen = row.count >= settingRow.regVerifyCount
}
if (row.type === verifyRecordType.ADD) {
addVerifyOpen = row.count >= settingRow.addVerifyCount
}
})
settingRow.regVerifyOpen = regVerifyOpen
settingRow.addVerifyOpen = addVerifyOpen
return settingRow; return settingRow;
}, },
@@ -50,28 +72,13 @@ const settingService = {
await this.refresh(c); await this.refresh(c);
}, },
async isAddEmail(c) {
const { addEmail, manyEmail } = await this.query(c);
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
},
async isRegisterVerify(c) {
const { registerVerify } = await this.query(c);
return registerVerify === settingConst.registerVerify.OPEN;
},
async isAddEmailVerify(c) {
const { addEmailVerify } = await this.query(c);
return addEmailVerify === settingConst.addEmailVerify.OPEN;
},
async setBackground(c, params) { async setBackground(c, params) {
const settingRow = await this.query(c); const settingRow = await this.query(c);
let { background } = params let { background } = params
if (!background.startsWith('http')) { if (background && !background.startsWith('http')) {
if (!c.env.r2) { if (!c.env.r2) {
throw new BizError(t('noOsUpBack')); throw new BizError(t('noOsUpBack'));
@@ -113,7 +120,9 @@ const settingService = {
}, },
async websiteConfig(c) { async websiteConfig(c) {
const settingRow = await this.get(c);
const settingRow = await this.get(c)
return { return {
register: settingRow.register, register: settingRow.register,
title: settingRow.title, title: settingRow.title,
@@ -128,7 +137,9 @@ const settingService = {
background: settingRow.background, background: settingRow.background,
loginOpacity: settingRow.loginOpacity, loginOpacity: settingRow.loginOpacity,
domainList:settingRow.domainList, domainList:settingRow.domainList,
regKey: settingRow.regKey regKey: settingRow.regKey,
regVerifyOpen: settingRow.regVerifyOpen,
addVerifyOpen: settingRow.addVerifyOpen
}; };
} }
}; };

View File

@@ -7,7 +7,7 @@ const turnstileService = {
async verify(c, token) { async verify(c, token) {
if (!token) { if (!token) {
throw new BizError(t('emptyBotToken')); throw new BizError(t('emptyBotToken'),400);
} }
const settingRow = await settingService.query(c) const settingRow = await settingService.query(c)

View File

@@ -0,0 +1,90 @@
import orm from '../entity/orm';
import verifyRecord from '../entity/verify-record';
import { eq, sql, and } from 'drizzle-orm';
import dayjs from 'dayjs';
import ipUtils from '../utils/ip-utils';
import { verifyRecordType } from '../const/entity-const';
const verifyRecordService = {
async selectListByIP(c) {
const ip = ipUtils.getIp(c)
return orm(c).select().from(verifyRecord).where(eq(verifyRecord.ip, ip)).all();
},
async clearRecord(c) {
await orm(c).delete(verifyRecord).run();
},
async isOpenRegVerify(c, regVerifyCount) {
const ip = ipUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.REG))).get();
if (row) {
if (row.count >= regVerifyCount){
return true
}
}
return false
},
async isOpenAddVerify(c, addVerifyCount) {
const ip = ipUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.ADD))).get();
if (row) {
if (row.count >= addVerifyCount){
return true
}
}
return false
},
async increaseRegCount(c) {
const ip = ipUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.REG))).get();
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
if (row) {
return orm(c).update(verifyRecord).set({
count: sql`${verifyRecord.count}
+ 1`, updateTime: now
}).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.REG))).returning().get();
} else {
return orm(c).insert(verifyRecord).values({ip, type: verifyRecordType.REG}).returning().run();
}
},
async increaseAddCount(c) {
const ip = ipUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.ADD))).get();
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
if (row) {
return orm(c).update(verifyRecord).set({
count: sql`${verifyRecord.count}
+ 1`, updateTime: now
}).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.ADD))).returning().get();
} else {
return orm(c).insert(verifyRecord).values({ip, type: verifyRecordType.ADD}).returning().get();
}
}
};
export default verifyRecordService;

View File

@@ -0,0 +1,9 @@
const ipUtils = {
getIp(c) {
return c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'Unknown';
}
}
export default ipUtils