新增支持API添加用户,查询邮件

This commit is contained in:
eoao
2025-08-10 11:37:46 +08:00
parent 021c3b23f5
commit ba6b02635b
38 changed files with 894 additions and 713 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
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-DQO7jFFS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BP2DuLPL.css">
<script type="module" crossorigin src="/assets/index-ONNky_gH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-NqVTnf-N.css">
</head>
<body>
<div id="loading-first">
+18
View File
@@ -0,0 +1,18 @@
import app from '../hono/hono';
import result from '../model/result';
import publicService from '../service/public-service';
app.post('/public/genToken', async (c) => {
const data = await publicService.genToken(c, await c.req.json());
return c.json(result.ok(data));
});
app.post('/public/emailList', async (c) => {
const list = await publicService.emailList(c, await c.req.json());
return c.json(result.ok(list));
});
app.post('/public/addUser', async (c) => {
await publicService.addUser(c, await c.req.json());
return c.json(result.ok());
});
+2 -1
View File
@@ -1,7 +1,8 @@
const KvConst = {
AUTH_INFO: 'auth-uid:',
SETTING: 'setting:',
SEND_DAY_COUNT: 'send_day_count:'
SEND_DAY_COUNT: 'send_day_count:',
PUBLIC_KEY: "public_key:"
}
export default KvConst;
+2 -1
View File
@@ -33,6 +33,7 @@ export const setting = sqliteTable('setting', {
noticeOffset: integer('notice_offset').default(0).notNull(),
noticeWidth: integer('notice_width').default(400).notNull(),
notice: integer('notice').default(0).notNull(),
noRecipient: integer('no_recipient').default(1).notNull()
noRecipient: integer('no_recipient').default(1).notNull(),
loginDomain: integer('login_domain').default(0).notNull()
});
export default setting
+1
View File
@@ -17,4 +17,5 @@ import '../api/all-email-api'
import '../api/init-api'
import '../api/analysis-api'
import '../api/reg-key-api'
import '../api/public-api'
export default app;
+3
View File
@@ -59,6 +59,9 @@ const en = {
noDomainPermRegKey: "Registration code not valid for this domain",
noDomainPermSend: "No permission to send from this domain email",
JWTMismatch: 'JWT secret mismatch',
publicTokenFail: 'Token validation failed',
notAdmin: 'The entered email is not an administrator email',
emailExistDatabase: 'Email already exists in the database',
perms: {
"邮件": "Email",
"邮件发送": "Send Email",
+1 -1
View File
@@ -21,7 +21,7 @@ const resources = {
};
i18next.init({
fallbackLng: 'en',
fallbackLng: 'zh',
resources,
});
+4 -1
View File
@@ -34,7 +34,7 @@ const zh = {
noRegKeyTotal: '注册码使用次数已耗尽',
regKeyExpire: '注册码已过期',
emailAndPwdEmpty: '邮箱和密码不能为空',
notExistUser: '邮箱不存在',
notExistUser: '输入的邮箱不存在',
isDelUser: '该邮箱已被注销',
isBanUser: '该邮箱已被禁用',
regKeyUseCount: '使用次数不能为空',
@@ -59,6 +59,9 @@ const zh = {
noDomainPermRegKey: '你的注册码没有权限注册该域名邮箱',
noDomainPermSend: '你没有权限使用该域名邮箱发送邮件',
JWTMismatch: 'jwt_secret 不匹配',
publicTokenFail: 'token验证失败',
notAdmin: '输入的邮箱不是管理员邮箱',
emailExistDatabase: '有邮箱已存在数据库中',
perms: {
"邮件": "邮件",
"邮件发送": "邮件发送",
+5
View File
@@ -20,10 +20,15 @@ const init = {
await this.v1_4DB(c);
await this.v1_5DB(c);
await this.v1_6DB(c);
await this.v1_7DB(c);
await settingService.refresh(c);
return c.text(t('initSuccess'));
},
async v1_7DB(c) {
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN login_domain INTEGER NOT NULL DEFAULT 0;`).run();
},
async v1_6DB(c) {
const noticeContent = '<div style="color: teal;margin-bottom: 5px;">欢迎使用 Cloud Mail 🎉 </div >\n' +
+12 -1
View File
@@ -14,7 +14,8 @@ const exclude = [
'/file',
'/setting/websiteConfig',
'/webhooks',
'/init'
'/init',
'/public/genToken'
];
const requirePerms = [
@@ -95,6 +96,16 @@ app.use('*', async (c, next) => {
return await next();
}
if (path.startsWith('/public')) {
const userPublicToken = await c.env.kv.get(KvConst.PUBLIC_KEY);
const publicToken = c.req.header(constant.TOKEN_HEADER);
if (publicToken !== userPublicToken) {
throw new BizError(t('publicTokenFail'), 401);
}
return await next();
}
const jwt = c.req.header(constant.TOKEN_HEADER);
@@ -152,6 +152,10 @@ const accountService = {
await orm(c).insert(account).values({ ...params }).returning();
},
async insertList(c, list) {
await orm(c).insert(account).values(list).run();
},
async physicsDeleteAll(c) {
const accountIdsRow = await orm(c).select({accountId: account.accountId}).from(account).where(eq(account.isDel,isDel.DELETE)).limit(99);
if (accountIdsRow.length === 0) {
+6 -2
View File
@@ -119,10 +119,10 @@ const loginService = {
const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType });
await userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email, name: emailUtils.getName(email) });
await userService.updateUserInfo(c, userId, true);
if (regKey !== settingConst.regKey.CLOSE && type) {
await regKeyService.reduceCount(c, code, 1);
}
@@ -136,6 +136,10 @@ const loginService = {
},
async registerVerify() {
},
async handleOpenRegKey(c, regKey, code) {
if (!code) {
+195
View File
@@ -0,0 +1,195 @@
import BizError from '../error/biz-error';
import orm from '../entity/orm';
import { v4 as uuidv4 } from 'uuid';
import { and, asc, desc, eq, sql } from 'drizzle-orm';
import saltHashUtils from '../utils/crypto-utils';
import cryptoUtils from '../utils/crypto-utils';
import emailUtils from '../utils/email-utils';
import roleService from './role-service';
import verifyUtils from '../utils/verify-utils';
import { t } from '../i18n/i18n';
import reqUtils from '../utils/req-utils';
import dayjs from 'dayjs';
import { isDel, roleConst } from '../const/entity-const';
import email from '../entity/email';
import userService from './user-service';
import KvConst from '../const/kv-const';
const publicService = {
async emailList(c, params) {
let { toEmail, content, subject, sendName, sendEmail, timeSort, num, size, type , isDel } = params
const query = orm(c).select({
emailId: email.emailId,
sendEmail: email.sendEmail,
sendName: email.name,
subject: email.subject,
toEmail: email.toEmail,
toName: email.toName,
type: email.type,
createTime: email.createTime,
content: email.content,
text: email.text,
isDel: email.isDel,
}).from(email)
if (!size) {
size = 20
}
if (!num) {
num = 1
}
size = Number(size);
num = Number(num);
num = (num - 1) * size;
let conditions = []
if (toEmail) {
conditions.push(sql`${email.toEmail} COLLATE NOCASE LIKE ${toEmail}`)
}
if (sendEmail) {
conditions.push(sql`${email.sendEmail} COLLATE NOCASE LIKE ${sendEmail}`)
}
if (sendName) {
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${sendName}`)
}
if (subject) {
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${subject}`)
}
if (content) {
conditions.push(sql`${email.content} COLLATE NOCASE LIKE ${content}`)
}
if (type || type === 0) {
conditions.push(eq(email.type, type))
}
if (isDel || isDel === 0) {
conditions.push(eq(email.isDel, isDel))
}
if (conditions.length === 1) {
query.where(...conditions)
} else if (conditions.length > 1) {
query.where(and(...conditions))
}
if (timeSort === 'asc') {
query.orderBy(asc(email.emailId));
} else {
query.orderBy(desc(email.emailId));
}
return query.limit(size).offset(num);
},
async addUser(c, params) {
const { list } = params;
if (list.length === 0) return;
for (const emailRow of list) {
if (!verifyUtils.isEmail(emailRow.email)) {
throw new BizError(t('notEmail'));
}
if (!c.env.domain.includes(emailUtils.getDomain(emailRow.email))) {
throw new BizError(t('notEmailDomain'));
}
const { salt, hash } = await saltHashUtils.hashPassword(
emailRow.password || cryptoUtils.genRandomPwd()
);
emailRow.salt = salt;
emailRow.hash = hash;
}
const activeIp = reqUtils.getIp(c);
const { os, browser, device } = reqUtils.getUserAgent(c);
const activeTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
const roleList = await roleService.roleSelectUse(c);
const defRole = roleList.find(roleRow => roleRow.isDefault === roleConst.isDefault.OPEN);
const userList = [];
for (const emailRow of list) {
let { email, hash, salt, roleName } = emailRow;
let type = defRole.roleId;
if (roleName) {
const roleRow = roleList.find(role => role.name === roleName);
type = roleRow ? roleRow.roleId : type;
}
const userSql = `INSERT INTO user (email, password, salt, type, os, browser, active_ip, create_ip, device, active_time, create_time)
VALUES ('${email}', '${hash}', '${salt}', '${type}', '${os}', '${browser}', '${activeIp}', '${activeIp}', '${device}', '${activeTime}', '${activeTime}')`
const accountSql = `INSERT INTO account (email, name, user_id)
VALUES ('${email}', '${emailUtils.getName(email)}', 0);`;
userList.push(c.env.db.prepare(userSql));
userList.push(c.env.db.prepare(accountSql));
}
userList.push(c.env.db.prepare(`UPDATE account SET user_id = (SELECT user_id FROM user WHERE user.email = account.email) WHERE user_id = 0;`))
try {
await c.env.db.batch(userList);
} catch (e) {
if(e.message.includes('SQLITE_CONSTRAINT')) {
throw new BizError(t('emailExistDatabase'))
} else {
throw e
}
}
},
async genToken(c, params) {
await this.verifyUser(c, params)
const uuid = uuidv4();
await c.env.kv.put(KvConst.PUBLIC_KEY, uuid, { expirationTtl: 60 * 60 * 24 * 7 });
return {token: uuid}
},
async verifyUser(c, params) {
const { email, password } = params
const userRow = await userService.selectByEmailIncludeDel(c, email);
if (email !== c.env.admin) {
throw new BizError(t('notAdmin'));
}
if (!userRow || userRow.isDel === isDel.DELETE) {
throw new BizError(t('notExistUser'));
}
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
throw new BizError(t('IncorrectPwd'));
}
}
}
export default publicService
+5 -1
View File
@@ -116,7 +116,7 @@ const roleService = {
},
roleSelectUse(c) {
return orm(c).select({ name: role.name, roleId: role.roleId }).from(role).orderBy(asc(role.sort)).all();
return orm(c).select({ name: role.name, roleId: role.roleId, isDefault: role.isDefault }).from(role).orderBy(asc(role.sort)).all();
},
async selectDefaultRole(c) {
@@ -170,6 +170,10 @@ const roleService = {
})
return availIndex > -1
},
selectByName(c, roleName) {
return orm(c).select().from(role).where(eq(role.name, roleName)).get();
}
};
@@ -148,6 +148,7 @@ const settingService = {
noticeWidth: settingRow.noticeWidth,
noticeOffset: settingRow.noticeOffset,
notice: settingRow.notice,
loginDomain: settingRow.loginDomain
};
}
};
+8 -35
View File
@@ -8,7 +8,6 @@ import kvConst from '../const/kv-const';
import KvConst from '../const/kv-const';
import cryptoUtils from '../utils/crypto-utils';
import emailService from './email-service';
import { UAParser } from 'ua-parser-js';
import dayjs from 'dayjs';
import permService from './perm-service';
import roleService from './role-service';
@@ -16,6 +15,7 @@ import emailUtils from '../utils/email-utils';
import saltHashUtils from '../utils/crypto-utils';
import constant from '../const/constant';
import { t } from '../i18n/i18n'
import reqUtils from '../utils/req-utils';
const userService = {
@@ -216,49 +216,22 @@ const userService = {
async updateUserInfo(c, userId, recordCreateIp = false) {
const ua = c.req.header('user-agent') || '';
console.log(ua);
const parser = new UAParser(ua);
const { browser, device, os } = parser.getResult();
let browserInfo = null;
let osInfo = null;
if (browser.name) {
browserInfo = browser.name + ' ' + browser.version;
}
const activeIp = reqUtils.getIp(c);
if (os.name) {
osInfo = os.name + os.version;
}
let deviceInfo = 'Desktop';
const hasVendor = !!device?.vendor;
const hasModel = !!device?.model;
if (hasVendor || hasModel) {
const vendor = device.vendor || '';
const model = device.model || '';
const type = device.type || '';
const namePart = [vendor, model].filter(Boolean).join(' ');
const typePart = type ? ` (${type})` : '';
deviceInfo = (namePart + typePart).trim();
}
const userIp = c.req.header('cf-connecting-ip') || '';
const {os, browser, device} = reqUtils.getUserAgent(c);
const params = {
os: osInfo,
browser: browserInfo,
device: deviceInfo,
activeIp: userIp,
os,
browser,
device,
activeIp,
activeTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
if (recordCreateIp) {
params.createIp = userIp;
params.createIp = activeIp;
}
await orm(c)
@@ -2,13 +2,13 @@ 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 reqUtils from '../utils/req-utils';
import { verifyRecordType } from '../const/entity-const';
const verifyRecordService = {
async selectListByIP(c) {
const ip = ipUtils.getIp(c)
const ip = reqUtils.getIp(c)
return orm(c).select().from(verifyRecord).where(eq(verifyRecord.ip, ip)).all();
},
@@ -18,7 +18,7 @@ const verifyRecordService = {
async isOpenRegVerify(c, regVerifyCount) {
const ip = ipUtils.getIp(c)
const ip = reqUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.REG))).get();
@@ -35,7 +35,7 @@ const verifyRecordService = {
async isOpenAddVerify(c, addVerifyCount) {
const ip = ipUtils.getIp(c)
const ip = reqUtils.getIp(c)
const row = await orm(c).select().from(verifyRecord).where(and(eq(verifyRecord.ip, ip),eq(verifyRecord.type,verifyRecordType.ADD))).get();
@@ -53,7 +53,7 @@ const verifyRecordService = {
async increaseRegCount(c) {
const ip = ipUtils.getIp(c)
const ip = reqUtils.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');
@@ -70,7 +70,7 @@ const verifyRecordService = {
async increaseAddCount(c) {
const ip = ipUtils.getIp(c)
const ip = reqUtils.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');
+9
View File
@@ -25,6 +25,15 @@ const saltHashUtils = {
async verifyPassword(inputPassword, salt, storedHash) {
const hash = await this.genHashPassword(inputPassword, salt);
return hash === storedHash;
},
genRandomPwd(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
};
-9
View File
@@ -1,9 +0,0 @@
const ipUtils = {
getIp(c) {
return c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'Unknown';
}
}
export default ipUtils
+45
View File
@@ -0,0 +1,45 @@
import { UAParser } from 'ua-parser-js';
const reqUtils = {
getIp(c) {
return c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'Unknown';
},
getUserAgent(c) {
const ua = c.req.header('user-agent') || '';
const parser = new UAParser(ua);
const { browser, device, os } = parser.getResult();
let browserInfo = null;
let osInfo = null;
if (browser.name) {
browserInfo = browser.name + ' ' + browser.version;
}
if (os.name) {
osInfo = os.name + os.version;
}
let deviceInfo = 'Desktop';
const hasVendor = !!device?.vendor;
const hasModel = !!device?.model;
if (hasVendor || hasModel) {
const vendor = device.vendor || '';
const model = device.model || '';
const type = device.type || '';
const namePart = [vendor, model].filter(Boolean).join(' ');
const typePart = type ? ` (${type})` : '';
deviceInfo = (namePart + typePart).trim();
}
return {browser: browserInfo || '', device: deviceInfo || '', os: osInfo || ''}
}
}
export default reqUtils
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-07-29"
compatibility_date = "2025-06-04"
keep_vars = true
[observability]
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail-dev"
main = "src/index.js"
compatibility_date = "2025-07-29"
compatibility_date = "2025-06-04"
keep_vars = true
[observability]
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail-test"
main = "src/index.js"
compatibility_date = "2025-07-29"
compatibility_date = "2025-06-04"
keep_vars = true
[observability]
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-07-29"
compatibility_date = "2025-06-04"
keep_vars = true
[observability]