feat: add UserLogin (#209)

This commit is contained in:
Dream Hunter
2024-05-08 23:14:44 +08:00
committed by GitHub
parent 55b2603913
commit 1fa56dfe98
57 changed files with 2300 additions and 285 deletions

View 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 });
},
}

View File

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

View File

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

View File

@@ -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',
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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, },

View 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
})
},
}

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

View 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
View 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
})
},
}

View File

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

View File

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