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

This commit is contained in:
eoao
2025-08-10 11:37:46 +08:00
parent 2437ff4599
commit 46127a8d67
38 changed files with 894 additions and 713 deletions

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());
});

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;

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

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;

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",

View File

@@ -21,7 +21,7 @@ const resources = {
};
i18next.init({
fallbackLng: 'en',
fallbackLng: 'zh',
resources,
});

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: {
"邮件": "邮件",
"邮件发送": "邮件发送",

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' +

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);

View File

@@ -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) {

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) {

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

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();
}
};

View File

@@ -148,6 +148,7 @@ const settingService = {
noticeWidth: settingRow.noticeWidth,
noticeOffset: settingRow.noticeOffset,
notice: settingRow.notice,
loginDomain: settingRow.loginDomain
};
}
};

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)

View File

@@ -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');

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;
}
};

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

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