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

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

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>
<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 type="module" crossorigin src="/assets/index-BOcWPX50.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CTj27R8v.css">
<script type="module" crossorigin src="/assets/index-BwB6muO3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEDjT-8v.css">
</head>
<body>
<div id="loading-first">

View File

@@ -1 +1,2 @@
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 = {
status: {
NORMAL: 0,
@@ -74,7 +76,7 @@ export const settingConst = {
},
addEmail: {
OPEN: 0,
CLOSE: 1,
CLOSE: 1
},
manyEmail: {
OPEN: 0,
@@ -83,10 +85,12 @@ export const settingConst = {
registerVerify: {
OPEN: 0,
CLOSE: 1,
COUNT: 2,
},
addEmailVerify: {
OPEN: 0,
CLOSE: 1,
COUNT: 2,
},
forwardStatus: {
OPEN: 0,
@@ -102,6 +106,11 @@ export const settingConst = {
}
}
export const verifyRecordType = {
REG: 0,
ADD: 1,
}
export const isDel = {
DELETE: 1,

View File

@@ -8,6 +8,8 @@ export const setting = sqliteTable('setting', {
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
addEmailVerify: integer('add_email_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(),
r2Domain: text('r2_domain'),
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',
noOsDomainUpBack: 'Cannot upload background: R2 domain not configured',
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',
authExpired: 'Authentication has expired. Please sign in again',
unauthorized: 'Unauthorized',

View File

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

View File

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

View File

@@ -18,10 +18,34 @@ const init = {
await this.v1_3_1DB(c);
await this.v1_4DB(c);
await this.v1_5DB(c);
await this.v1_6DB(c);
await settingService.refresh(c);
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) {
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();

View File

@@ -6,21 +6,26 @@ import emailService from './email-service';
import orm from '../entity/orm';
import account from '../entity/account';
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 turnstileService from './turnstile-service';
import roleService from './role-service';
import { t } from '../i18n/i18n';
import verifyRecordService from './verify-record-service';
const accountService = {
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'));
}
let { email, token } = params;
if (!email) {
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) {
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);
}
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) {

View File

@@ -18,6 +18,7 @@ import regKeyService from './reg-key-service';
import dayjs from 'dayjs';
import { toUtc } from '../utils/date-uitil';
import { t } from '../i18n/i18n.js';
import verifyRecordService from './verify-record-service';
const loginService = {
@@ -25,11 +26,8 @@ const loginService = {
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) {
throw new BizError(t('regDisabled'));
@@ -80,7 +78,6 @@ const loginService = {
throw new BizError(t('isRegAccount'));
}
const { salt, hash } = await saltHashUtils.hashPassword(password);
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 });
await userService.updateUserInfo(c, userId, true);
@@ -114,6 +127,13 @@ const loginService = {
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) {

View File

@@ -1,7 +1,7 @@
import KvConst from '../const/kv-const';
import setting from '../entity/setting';
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 r2Service from './r2-service';
import emailService from './email-service';
@@ -10,6 +10,7 @@ import userService from './user-service';
import constant from '../const/constant';
import BizError from '../error/biz-error';
import { t } from '../i18n/i18n'
import verifyRecordService from './verify-record-service';
const settingService = {
@@ -31,11 +32,32 @@ const settingService = {
},
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;
Object.keys(settingRow.resendTokens).forEach(key => {
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;
},
@@ -50,28 +72,13 @@ const settingService = {
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) {
const settingRow = await this.query(c);
let { background } = params
if (!background.startsWith('http')) {
if (background && !background.startsWith('http')) {
if (!c.env.r2) {
throw new BizError(t('noOsUpBack'));
@@ -113,7 +120,9 @@ const settingService = {
},
async websiteConfig(c) {
const settingRow = await this.get(c);
const settingRow = await this.get(c)
return {
register: settingRow.register,
title: settingRow.title,
@@ -128,7 +137,9 @@ const settingService = {
background: settingRow.background,
loginOpacity: settingRow.loginOpacity,
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) {
if (!token) {
throw new BizError(t('emptyBotToken'));
throw new BizError(t('emptyBotToken'),400);
}
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