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