mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-24 17:49:52 +08:00
feat: add UserLogin (#209)
This commit is contained in:
135
worker/src/admin_api/admin_user_api.js
Normal file
135
worker/src/admin_api/admin_user_api.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
|
||||
export default {
|
||||
getSetting: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
return c.json(settings)
|
||||
},
|
||||
saveSetting: async (c) => {
|
||||
const value = await c.req.json();
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.enableMailVerify && !c.env.KV) {
|
||||
return c.text("Please enable KV first if you want to enable mail verify", 403)
|
||||
}
|
||||
if (settings.enableMailVerify) {
|
||||
const mailDomain = settings.verifyMailSender.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
}
|
||||
}
|
||||
if (settings.maxAddressCount < 0) {
|
||||
return c.text("Invalid maxAddressCount", 400)
|
||||
}
|
||||
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
},
|
||||
getUsers: async (c) => {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
if (query) {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` where u.user_email like ?`
|
||||
+ ` order by u.id desc limit ? offset ?`
|
||||
).bind(`%${query}%`, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: userCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM users where user_email like ?`
|
||||
).bind(`%${query}%`).first();
|
||||
count = userCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` order by u.id desc limit ? offset ?`
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: userCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM users`
|
||||
).first();
|
||||
count = userCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
},
|
||||
createUser: async (c) => {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
const userInfo = new UserInfo(geoData);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("User already exists", 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
deleteUser: async (c) => {
|
||||
const { user_id } = c.req.param();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users WHERE id = ?`
|
||||
).bind(user_id).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success || !addressSuccess) {
|
||||
return c.text("Failed to delete user", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
resetPassword: async (c) => {
|
||||
const { user_id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE users SET password = ? WHERE id = ?`
|
||||
).bind(password, user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text(`Failed to reset password: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
|
||||
export default {
|
||||
cleanup: async (c) => {
|
||||
@@ -15,11 +16,13 @@ export default {
|
||||
},
|
||||
getCleanup: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
||||
return c.json(value || {})
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
return c.json(cleanupSetting)
|
||||
},
|
||||
saveCleanup: async (c) => {
|
||||
const value = await c.req.json();
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(value));
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||
return c.json({ success: true })
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from './utils'
|
||||
import { newAddress } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import cleanup_api from './admin/cleanup_api'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
||||
import { newAddress } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import cleanup_api from './cleanup_api'
|
||||
import admin_user_api from './admin_user_api'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -83,8 +84,11 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
`DELETE FROM address_sender WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
const { success: usersAddressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ?`
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess
|
||||
success: success && mailSuccess && sendAccess && usersAddressSuccess
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,11 +102,10 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
address_id: id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
password: jwt
|
||||
jwt: jwt
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
@@ -296,14 +299,11 @@ api.get('/admin/statistics', async (c) => {
|
||||
})
|
||||
});
|
||||
|
||||
api.post('/admin/cleanup', cleanup_api.cleanup)
|
||||
api.get('/admin/auto_cleanup', cleanup_api.getCleanup)
|
||||
api.post('/admin/auto_cleanup', cleanup_api.saveCleanup)
|
||||
|
||||
|
||||
api.get('/admin/account_settings', async (c) => {
|
||||
try {
|
||||
/** @type {Array<string>|undefined|null} */
|
||||
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
/** @type {Array<string>|undefined|null} */
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
return c.json({
|
||||
blockList: blockList || [],
|
||||
@@ -316,6 +316,7 @@ api.get('/admin/account_settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/account_settings', async (c) => {
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const { blockList, sendBlockList } = await c.req.json();
|
||||
if (!blockList || !sendBlockList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
@@ -333,4 +334,14 @@ api.post('/admin/account_settings', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.post('/admin/cleanup', cleanup_api.cleanup)
|
||||
api.get('/admin/auto_cleanup', cleanup_api.getCleanup)
|
||||
api.post('/admin/auto_cleanup', cleanup_api.saveCleanup)
|
||||
api.get('/admin/user_settings', admin_user_api.getSetting)
|
||||
api.post('/admin/user_settings', admin_user_api.saveSetting)
|
||||
api.get('/admin/users', admin_user_api.getUsers)
|
||||
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
||||
api.post('/admin/users', admin_user_api.createUser)
|
||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||
|
||||
export { api }
|
||||
@@ -2,4 +2,5 @@ export const CONSTANTS = {
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
SEND_BLOCK_LIST_KEY: 'send_block_list',
|
||||
AUTO_CLEANUP_KEY: 'auto_cleanup',
|
||||
USER_SETTINGS_KEY: 'user_settings',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { EmailMessage } from "cloudflare:email";
|
||||
import { getBooleanValue } from "./utils";
|
||||
|
||||
async function email(message, env, ctx) {
|
||||
@@ -40,7 +39,7 @@ async function email(message, env, ctx) {
|
||||
contentType: 'text/plain',
|
||||
data: results.message || "This is an auto-reply message, please reconact later."
|
||||
});
|
||||
|
||||
const { EmailMessage } = await import('cloudflare:email');
|
||||
const replyMessage = new EmailMessage(
|
||||
message.to,
|
||||
message.from,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { address } = c.get("jwtPayload");
|
||||
const { auto_reply } = await c.req.json();
|
||||
const { name, subject, source_prefix, message, enabled } = auto_reply;
|
||||
if ((!subject || !message) && enabled) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import auto_reply from './user/auto_reply'
|
||||
import auto_reply from './auto_reply'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -29,37 +30,43 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
const sendMail = async (c, address) => {
|
||||
export const sendMail = async (c, address, reqJson) => {
|
||||
if (!address) {
|
||||
throw new Error("No address")
|
||||
}
|
||||
// check domain
|
||||
const mailDomain = address.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
// check permission
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
return c.text("No balance", 400);
|
||||
throw new Error("No balance")
|
||||
}
|
||||
let {
|
||||
from_name, to_mail, to_name,
|
||||
subject, content, is_html
|
||||
} = await c.req.json();
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
} = reqJson;
|
||||
if (!to_mail) {
|
||||
return c.text("Invalid to mail", 400)
|
||||
throw new Error("Invalid to mail")
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
return c.text("to_mail address is blocked", 400);
|
||||
throw new Error("to_mail address is blocked")
|
||||
}
|
||||
from_name = from_name || address;
|
||||
to_name = to_name || to_mail;
|
||||
if (!subject) {
|
||||
return c.text("Invalid subject", 400)
|
||||
throw new Error("Invalid subject")
|
||||
}
|
||||
if (!content) {
|
||||
return c.text("Invalid content", 400)
|
||||
throw new Error("Invalid content")
|
||||
}
|
||||
let dmikBody = {}
|
||||
if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) {
|
||||
@@ -100,7 +107,7 @@ const sendMail = async (c, address) => {
|
||||
const respText = await resp.text();
|
||||
console.log(resp.status + " " + resp.statusText + ": " + respText);
|
||||
if (resp.status >= 300) {
|
||||
return c.text("Failed to send mail", 500)
|
||||
throw new Error(`Mailchannels error: ${resp.status} ${respText}`);
|
||||
}
|
||||
// update balance
|
||||
try {
|
||||
@@ -120,7 +127,8 @@ const sendMail = async (c, address) => {
|
||||
delete body.personalizations[0].dkim_private_key;
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
body.reqIp = reqIp;
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
body.geoData = geoData;
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
||||
).bind(address, JSON.stringify(body)).run();
|
||||
@@ -130,12 +138,18 @@ const sendMail = async (c, address) => {
|
||||
} catch (e) {
|
||||
console.warn(`Failed to save to sendbox for ${address}`);
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
api.post('/api/send_mail', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
return await sendMail(c, address);
|
||||
const reqJson = await c.req.json();
|
||||
try {
|
||||
await sendMail(c, address, reqJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
api.post('/external/api/send_mail', async (c) => {
|
||||
@@ -145,10 +159,11 @@ api.post('/external/api/send_mail', async (c) => {
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
return await sendMail(c, address);
|
||||
const reqJson = await c.req.json();
|
||||
return await sendMail(c, address, reqJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to verify token", e);
|
||||
return c.text("Unauthorized", 401)
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
92
worker/src/models/index.js
Normal file
92
worker/src/models/index.js
Normal file
@@ -0,0 +1,92 @@
|
||||
export class UserSettings {
|
||||
/** @param {UserSettings|undefined|null} data */
|
||||
constructor(data) {
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
enable, enableMailVerify, verifyMailSender,
|
||||
enableMailAllowList, mailAllowList, maxAddressCount
|
||||
} = data || {};
|
||||
/** @type {boolean|undefined} */
|
||||
this.enable = enable;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailVerify = enableMailVerify;
|
||||
/** @type {string|undefined} */
|
||||
this.verifyMailSender = verifyMailSender;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailAllowList = enableMailAllowList;
|
||||
/** @type {Array<string>|undefined} */
|
||||
this.mailAllowList = mailAllowList;
|
||||
/** @type {number|undefined} */
|
||||
this.maxAddressCount = maxAddressCount || 5;
|
||||
}
|
||||
}
|
||||
|
||||
export class CleanupSettings {
|
||||
/** @param {CleanupSettings|undefined|null} data */
|
||||
constructor(data) {
|
||||
const {
|
||||
enableMailsAutoCleanup, cleanMailsDays,
|
||||
enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
|
||||
enableAddressAutoCleanup, cleanAddressDays,
|
||||
enableSendBoxAutoCleanup, cleanSendBoxDays
|
||||
} = data || {};
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailsAutoCleanup = enableMailsAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanMailsDays = cleanMailsDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanUnknowMailsDays = cleanUnknowMailsDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableAddressAutoCleanup = enableAddressAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanAddressDays = cleanAddressDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanSendBoxDays = cleanSendBoxDays;
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoData {
|
||||
/** @param {string} ip @param {GeoData|undefined|null} data */
|
||||
constructor(ip, data) {
|
||||
const {
|
||||
country, city, timezone, postalCode, region,
|
||||
latitude, longitude, regionCode, asOrganization
|
||||
} = data || {};
|
||||
/** @type {string} */
|
||||
this.ip = ip;
|
||||
/** @type {string|undefined} */
|
||||
this.country = country;
|
||||
/** @type {string|undefined} */
|
||||
this.city = city;
|
||||
/** @type {string|undefined} */
|
||||
this.timezone = timezone;
|
||||
/** @type {string|undefined} */
|
||||
this.postalCode = postalCode;
|
||||
/** @type {string|undefined} */
|
||||
this.region = region;
|
||||
/** @type {number|undefined} */
|
||||
this.latitude = latitude;
|
||||
/** @type {number|undefined} */
|
||||
this.longitude = longitude;
|
||||
/** @type {string|undefined} */
|
||||
this.regionCode = regionCode;
|
||||
/** @type {string|undefined} */
|
||||
this.asOrganization = asOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserInfo {
|
||||
/** @param {GeoData} geoData @param {string} userEmail */
|
||||
constructor(geoData, userEmail) {
|
||||
/** @type {geoData} */
|
||||
this.geoData = geoData;
|
||||
/** @type {string} */
|
||||
this.userEmail = userEmail;
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ api.delete('/api/delete_address', async (c) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
let name = address;
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
@@ -170,8 +170,11 @@ api.delete('/api/delete_address', async (c) => {
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ?`
|
||||
).bind(address_id).run();
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess
|
||||
success: success && mailSuccess && sendAccess && addressSuccess
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
|
||||
export async function scheduled(event, env, ctx) {
|
||||
console.log("Scheduled event: ", event);
|
||||
let autoCleanupSetting = await getJsonSetting(
|
||||
const value = await getJsonSetting(
|
||||
{ env: env, },
|
||||
CONSTANTS.AUTO_CLEANUP_KEY
|
||||
);
|
||||
const autoCleanupSetting = new CleanupSettings(value);
|
||||
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
|
||||
autoCleanupSetting = autoCleanupSetting || {};
|
||||
if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) {
|
||||
await cleanup(
|
||||
{ env: env, },
|
||||
|
||||
139
worker/src/user_api/bind_address.js
Normal file
139
worker/src/user_api/bind_address.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export default {
|
||||
bind: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = c.get("jwtPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("No address or user token", 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).first("user_id");
|
||||
if (db_user_address_id) return c.json({ success: true })
|
||||
// check if binded address count
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.maxAddressCount > 0) {
|
||||
const { count } = await c.env.DB.prepare(
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(user_id).first();
|
||||
if (count >= settings.maxAddressCount) {
|
||||
return c.text("Max address count reached", 400)
|
||||
}
|
||||
}
|
||||
// bind
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
unbind: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = await c.req.json();
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// unbind
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Invalid address token", 400)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
getBindedAddresses: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!user_id) {
|
||||
return c.text("No user token", 400)
|
||||
}
|
||||
// select binded address
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT a.*,`
|
||||
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
|
||||
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
|
||||
+ ` FROM address a `
|
||||
+ ` JOIN users_address ua `
|
||||
+ ` ON ua.address_id = a.id `
|
||||
+ ` WHERE ua.user_id = ?`
|
||||
+ ` ORDER BY a.id DESC`
|
||||
).bind(user_id).all();
|
||||
return c.json({
|
||||
results: results,
|
||||
})
|
||||
},
|
||||
getBindedAddressJwt: async (c) => {
|
||||
const { address_id } = c.req.param();
|
||||
// check binded
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
}
|
||||
// check users_address if address binded
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?`
|
||||
).bind(address_id, user_id).first("user_id");
|
||||
if (!db_user_id) {
|
||||
return c.text("Address not binded", 400)
|
||||
}
|
||||
// generate jwt
|
||||
const name = await c.env.DB.prepare(
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
const jwt = await Jwt.sign({
|
||||
address: name,
|
||||
address_id: address_id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
jwt: jwt
|
||||
})
|
||||
},
|
||||
}
|
||||
19
worker/src/user_api/index.js
Normal file
19
worker/src/user_api/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import settings from './settings';
|
||||
import user from './user';
|
||||
import bind_address from './bind_address';
|
||||
|
||||
const api = new Hono();
|
||||
|
||||
api.get('/user_api/open_settings', settings.openSettings);
|
||||
api.get('/user_api/settings', settings.settings);
|
||||
api.post('/user_api/login', user.login);
|
||||
api.post('/user_api/verify_code', user.verifyCode);
|
||||
api.post('/user_api/register', user.register);
|
||||
api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
||||
api.post('/user_api/bind_address', bind_address.bind);
|
||||
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
||||
api.post('/user_api/unbind_address', bind_address.unbind);
|
||||
|
||||
export { api }
|
||||
25
worker/src/user_api/settings.js
Normal file
25
worker/src/user_api/settings.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export default {
|
||||
openSettings: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
return c.json({
|
||||
enable: settings.enable,
|
||||
enableMailVerify: settings.enableMailVerify,
|
||||
})
|
||||
},
|
||||
settings: async (c) => {
|
||||
const user = c.get("userPayload");
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user.user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400);
|
||||
}
|
||||
return c.json(user);
|
||||
},
|
||||
}
|
||||
141
worker/src/user_api/user.js
Normal file
141
worker/src/user_api/user.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { GeoData, UserInfo, UserSettings } from "../models";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
|
||||
export default {
|
||||
verifyCode: async (c) => {
|
||||
const { email, cf_token } = await c.req.json();
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
}
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
// check mail domain allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
if (settings.enableMailAllowList
|
||||
&& settings.mailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// check if code exists in KV
|
||||
const tmpcode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (tmpcode) {
|
||||
return c.text("Code already sent, please wait", 400)
|
||||
}
|
||||
// generate code 6 digits and convert to string
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
// send code to email
|
||||
try {
|
||||
await sendMail(c, settings.verifyMailSender, {
|
||||
to_mail: email,
|
||||
subject: "Temp Mail Verify code",
|
||||
content: `Your verify code is ${code}`,
|
||||
})
|
||||
} catch (e) {
|
||||
return c.text(`Failed to send verify code: ${e.message}`, 500)
|
||||
}
|
||||
// save to KV
|
||||
await c.env.KV.put(`temp-mail:${email}`, code, { expirationTtl: 300 });
|
||||
return c.json({
|
||||
success: true,
|
||||
expirationTtl: 300
|
||||
})
|
||||
},
|
||||
register: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
// check enable
|
||||
if (!settings.enable) {
|
||||
return c.text("User registration is disabled");
|
||||
}
|
||||
// check request
|
||||
const { email, password, code } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
}
|
||||
checkUserPassword(password);
|
||||
if (settings.enableMailVerify && !code) {
|
||||
return c.text("Need verify code", 400)
|
||||
}
|
||||
// check mail domain allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
if (settings.enableMailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// check code
|
||||
if (settings.enableMailVerify) {
|
||||
const verifyCode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (verifyCode != code) {
|
||||
return c.text("Invalid verify code", 400)
|
||||
}
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
const userInfo = new UserInfo(geoData);
|
||||
// if not enable mail verify, do not on conflict update
|
||||
if (!settings.enableMailVerify) {
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("User already exists, please login", 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
// if enable mail verify, on conflict update
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
+ ` ON CONFLICT(user_email) DO UPDATE SET password = ?, user_info = ?, updated_at = datetime('now')`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo),
|
||||
password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
login: async (c) => {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.text("Invalid email or password", 400);
|
||||
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
|
||||
`SELECT id, password FROM users where user_email = ?`
|
||||
).bind(email).first() || {};
|
||||
if (!dbPassword) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// TODO: need check password use random salt
|
||||
if (dbPassword != password) {
|
||||
return c.text("Invalid password", 400)
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
user_email: email,
|
||||
user_id: user_id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
jwt: jwt
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -153,3 +153,10 @@ export const checkCfTurnstile = async (c, token) => {
|
||||
throw new Error("Captcha failed");
|
||||
}
|
||||
}
|
||||
|
||||
export const checkUserPassword = (password) => {
|
||||
if (!password || password.length < 1 || password.length > 100) {
|
||||
throw new Error("Invalid password")
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { api } from './router';
|
||||
import { api as MailsApi } from './mails_api'
|
||||
import { api as userApi } from './user_api';
|
||||
import { api as adminApi } from './admin_api';
|
||||
import { api as apiV1 } from './api_v1';
|
||||
import { api as apiSendMail } from './send_mail_api'
|
||||
import { api as apiV1 } from './deprecated';
|
||||
import { api as apiSendMail } from './mails_api/send_mail_api'
|
||||
|
||||
import { email } from './email';
|
||||
import { scheduled } from './scheduled';
|
||||
import { getAdminPasswords, getPasswords } from './utils';
|
||||
|
||||
const app = new Hono()
|
||||
//cors
|
||||
app.use('/*', cors());
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address") || c.req.path.startsWith("/api/send_mail")) {
|
||||
// rate limit
|
||||
app.use('/*', async (c, next) => {
|
||||
if (
|
||||
c.req.path.startsWith("/api/new_address")
|
||||
|| c.req.path.startsWith("/api/send_mail")
|
||||
|| c.req.path.startsWith("/external/api/send_mail")
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
) {
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
if (reqIp && c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
@@ -33,13 +36,51 @@ app.use('/api/*', async (c, next) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
await next()
|
||||
});
|
||||
// api auth
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
||||
});
|
||||
|
||||
// user_api auth
|
||||
app.use('/user_api/*', async (c, next) => {
|
||||
if (
|
||||
c.req.path.startsWith("/user_api/open_settings")
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/login")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET);
|
||||
c.set("userPayload", payload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.text("Need User Token", 401)
|
||||
}
|
||||
if (c.req.path.startsWith('/user_api/bind_address')
|
||||
&& c.req.method === 'POST'
|
||||
) {
|
||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
// admin auth
|
||||
app.use('/admin/*', async (c, next) => {
|
||||
// check header x-admin-auth
|
||||
const adminPasswords = getAdminPasswords(c);
|
||||
@@ -55,6 +96,7 @@ app.use('/admin/*', async (c, next) => {
|
||||
|
||||
|
||||
app.route('/', api)
|
||||
app.route('/', MailsApi)
|
||||
app.route('/', userApi)
|
||||
app.route('/', adminApi)
|
||||
app.route('/', apiV1)
|
||||
|
||||
Reference in New Issue
Block a user