diff --git a/handler/users.js b/handler/users.js index a601c0c..8b7eddb 100644 --- a/handler/users.js +++ b/handler/users.js @@ -6,111 +6,131 @@ class HandlerUser { constructor() { } - // 获取微信的手机号码 - async wxGetPhoneNumber(ctx) { + // 生成 token + async genToken(uid) { + const crypto = await import("crypto"); + const hash = crypto.createHash("md5"); + hash.update(uid + Date.now() + Math.random()); + return hash.digest("hex"); + } + + checkUserValid(user) { + const isTokenValid = user.security.token && + user.security.tokenExpiry && + new Date() < user.security.tokenExpiry; + return isTokenValid; + } + + // 用户注册 + async register(ctx) { try { - const { code, appId } = ctx.request.body; - if (!code || !appId) { - return ResponseUtil.badRequest(ctx, "缺少手机号凭证 code 或 appId"); + const { userInfo } = ctx.request.body; + if (!userInfo) { + return ResponseUtil.badRequest(ctx, "缺少用户信息"); } - let app = config.app[appId]; - if (!app) { - return ResponseUtil.badRequest(ctx, `未配置 appId: ${appId}`); + const mobile = userInfo.profile?.mobile; + const passwd = userInfo.security?.passwd; + if (!mobile) { + return ResponseUtil.badRequest(ctx, "缺少手机号"); + } + if (!passwd) { + return ResponseUtil.badRequest(ctx, "缺少密码"); } - // 获取access_token - const client_credential_url = `https://api.weixin.qq.com/cgi-bin/token?appid=${app.appid}&secret=${app.secret}&grant_type=client_credential`; - const fetch = (await import("node-fetch")).default; - let sessionRes = await fetch(client_credential_url); - const resp = await sessionRes.json(); - if (!resp.access_token) { - return ResponseUtil.internalError(ctx, "获取微信 access_token 失败"); + // 检查手机号是否已注册 + const existingUser = await DBModel.User.findOne({ "profile.mobile": mobile }); + if (existingUser) { + return ResponseUtil.error(ctx, "手机号已注册", null, 409); } - // 获取phoneNumber - const phoneUrl = `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${resp.access_token}`; - const phoneRes = await fetch(phoneUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: code }) - }); - const phoneData = await phoneRes.json(); - if (phoneData.errcode) { - return ResponseUtil.error(ctx, `获取手机号失败: ${phoneData.errmsg}`, null, 400); + // 生成随机盐值 + const crypto = await import("crypto"); + const salt = crypto.randomBytes(8).toString("hex"); + + // MD5 加密密码 + const hash = crypto.createHash("md5"); + hash.update(passwd + salt); + const encryptedPasswd = hash.digest("hex"); + + // 构建新用户,结构同 UserSchema + const newUser = { + profile: userInfo.profile, + security: { + passwd: encryptedPasswd, + passwdSalt: salt, + }, + location: { + province: userInfo.location?.province || '', + city: userInfo.location?.city || '', + district: userInfo.location?.district || '', + }, + addresses: userInfo.addresses || [], + social: { + wechat: { + } + }, + status: { + account: "normal", + }, + app: userInfo.app || {}, + }; + + const user = await DBModel.User.setUser(newUser); + if (!user) { + return ResponseUtil.internalError(ctx, "注册失败"); } - // 从上图获取手机号 - const phoneNumber = phoneData.phone_info?.phoneNumber; - return ResponseUtil.success(ctx, { phoneNumber }, "获取手机号成功"); + // 生成 token + const token = await this.genToken(user._id.toString()); + user.security.token = token; + user.security.tokenExpiry = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000); + await user.save(); + + // 安全起见删除密码相关字段 + const safeUser = user.toObject(); + delete safeUser.security?.passwd; + delete safeUser.security?.passwdSalt; + + return ResponseUtil.success(ctx, { user: safeUser }, "注册成功"); } catch (err) { return ResponseUtil.internalError(ctx, err.message); } } - // 微信登录 - async wxSignin(ctx) { + // 手机号码登录 + async signin(ctx) { try { - const { code, phoneNumber, name, appId } = ctx.request.body; - if (!code || !appId) { - return ResponseUtil.badRequest(ctx, "缺少微信登录凭证 code 或 appId"); + const { mobile, passwd } = ctx.request.body; + if (!mobile) { + return ResponseUtil.badRequest(ctx, "缺少手机号"); + } + if (!passwd) { + return ResponseUtil.badRequest(ctx, "缺少密码"); } - - - let app = config.app[appId]; - if (!app) { - return ResponseUtil.badRequest(ctx, `未配置 appId: ${appId}`); - } - - // 通过 code 换取 openid/session_key - const sessionUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${app.appid}&secret=${app.secret}&js_code=${code}&grant_type=authorization_code`; - const wxSessionRes = await fetch(sessionUrl); - const sessionData = await wxSessionRes.json(); - if (sessionData.errcode) { - return ResponseUtil.error(ctx, `微信接口错误: ${sessionData.errmsg}`, null, 400); - } - - const { openid } = sessionData; - if (!openid) { - return ResponseUtil.error(ctx, "微信登录失败,未获取到 openid", null, 400); - } - - // 使用openid和phoneNumber查询用户 - let key = `app.${appId}.wxopenid`; - let user = await DBModel.User.findOne({ [key]: openid }); + // 查找用户 + let user = await DBModel.User.findOne({ "profile.mobile": mobile }); if (!user) { - if (!phoneNumber) { - return ResponseUtil.badRequest(ctx, "缺少手机号"); - } - - user = await DBModel.User.findOne({ "profile.mobile": phoneNumber }); - if (!user) { - const newUser = { - profile: { name: name || phoneNumber, mobile: phoneNumber, }, - status: { account: "normal", }, - app: {}, - }; - newUser.app[appId] = { role: ["user"], wxopenid: openid }; - - user = await DBModel.User.setUser(newUser); - } + return ResponseUtil.unauthorized(ctx, "用户不存在"); } - // - if (user) { - if (phoneNumber && phoneNumber.length > 0 && user.profile.mobile !== phoneNumber) { - user.profile.mobile = phoneNumber; - } - if (!(appId in user.app)) { - user.app[appId] = { role: ["user"], wxopenid: openid }; - } - user.app[appId].wxopenid = openid; - } else { - return ResponseUtil.internalError(ctx, "用户不存在"); + // 校验密码(MD5 加密比对) + const crypto = await import("crypto"); + const hash = crypto.createHash("md5"); + hash.update(passwd + (user.security.passwdSalt || "")); + const encryptedPasswd = hash.digest("hex"); + + if (user.security.passwd !== encryptedPasswd) { + return ResponseUtil.unauthorized(ctx, "密码错误"); } - // 更新Token + // 检查账户状态 + if (user.status.account === "lock") { + return ResponseUtil.forbidden(ctx, "账户已被锁定"); + } + + // 生成/更新 token const isTokenValid = user.security.token && user.security.tokenExpiry && new Date() < user.security.tokenExpiry; @@ -123,58 +143,11 @@ class HandlerUser { await user.save(); // 安全起见删除密码相关字段 - delete user.security.passwd; - delete user.security.passwdSalt; - - return ResponseUtil.success(ctx, { user }, "登录成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - // update user - async updateUser(ctx) { - try { - const userInfo = ctx.request.body; - if (!userInfo) { - return ResponseUtil.badRequest(ctx, "缺少用户信息"); - } - - // 从 token 获取当前用户 - const token = ctx.request.body?.token - || ctx.request.query?.token - || ctx.header?.authorization - || ctx.header?.token; - - if (!token) { - return ResponseUtil.badRequest(ctx, "缺少 token"); - } - - const user = await DBModel.User.findOne({ "security.token": token }); - if (!user) { - return ResponseUtil.unauthorized(ctx, "用户未登录或 token 无效"); - } - - // 检查 token 是否过期 - if (user.security.tokenExpiry && new Date() > user.security.tokenExpiry) { - return ResponseUtil.unauthorized(ctx, "登录已过期,请重新登录"); - } - - const updatedUser = await DBModel.User.updateFromUserInfo( - user._id, - userInfo - ); - - if (!updatedUser) { - return ResponseUtil.internalError(ctx, "更新用户失败"); - } - - // 安全起见删除密码相关字段 - const safeUser = updatedUser.toObject(); + const safeUser = user.toObject(); delete safeUser.security?.passwd; delete safeUser.security?.passwdSalt; - return ResponseUtil.success(ctx, { user: safeUser }, "更新成功"); + return ResponseUtil.success(ctx, { user: safeUser }, "登录成功"); } catch (err) { return ResponseUtil.internalError(ctx, err.message); } @@ -238,6 +211,54 @@ class HandlerUser { } } + // update user + async updateUser(ctx) { + try { + const userInfo = ctx.request.body; + if (!userInfo) { + return ResponseUtil.badRequest(ctx, "缺少用户信息"); + } + + // 从 token 获取当前用户 + const token = ctx.request.body?.token + || ctx.request.query?.token + || ctx.header?.authorization + || ctx.header?.token; + + if (!token) { + return ResponseUtil.badRequest(ctx, "缺少 token"); + } + + const user = await DBModel.User.findOne({ "security.token": token }); + if (!user) { + return ResponseUtil.unauthorized(ctx, "用户未登录或 token 无效"); + } + + // 检查 token 是否过期 + if (user.security.tokenExpiry && new Date() > user.security.tokenExpiry) { + return ResponseUtil.unauthorized(ctx, "登录已过期,请重新登录"); + } + + const updatedUser = await DBModel.User.updateFromUserInfo( + user._id, + userInfo + ); + + if (!updatedUser) { + return ResponseUtil.internalError(ctx, "更新用户失败"); + } + + // 安全起见删除密码相关字段 + const safeUser = updatedUser.toObject(); + delete safeUser.security?.passwd; + delete safeUser.security?.passwdSalt; + + return ResponseUtil.success(ctx, { user: safeUser }, "更新成功"); + } catch (err) { + return ResponseUtil.internalError(ctx, err.message); + } + } + // 获取用户列表 async userList(ctx) { try { @@ -282,12 +303,130 @@ class HandlerUser { } } - // 生成 token - async genToken(uid) { - const crypto = await import("crypto"); - const hash = crypto.createHash("md5"); - hash.update(uid + Date.now() + Math.random()); - return hash.digest("hex"); + // 微信登录 + async wxSignin(ctx) { + try { + const { code, phoneNumber, name, appId } = ctx.request.body; + if (!code || !appId) { + return ResponseUtil.badRequest(ctx, "缺少微信登录凭证 code 或 appId"); + } + + + + let app = config.app[appId]; + if (!app) { + return ResponseUtil.badRequest(ctx, `未配置 appId: ${appId}`); + } + + // 通过 code 换取 openid/session_key + const sessionUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${app.appid}&secret=${app.secret}&js_code=${code}&grant_type=authorization_code`; + const wxSessionRes = await fetch(sessionUrl); + const sessionData = await wxSessionRes.json(); + if (sessionData.errcode) { + return ResponseUtil.error(ctx, `微信接口错误: ${sessionData.errmsg}`, null, 400); + } + + const { openid } = sessionData; + if (!openid) { + return ResponseUtil.error(ctx, "微信登录失败,未获取到 openid", null, 400); + } + + // 使用openid和phoneNumber查询用户 + let key = `app.${appId}.wxopenid`; + let user = await DBModel.User.findOne({ [key]: openid }); + if (!user) { + if (!phoneNumber) { + return ResponseUtil.badRequest(ctx, "缺少手机号"); + } + + user = await DBModel.User.findOne({ "profile.mobile": phoneNumber }); + if (!user) { + const newUser = { + profile: { name: name || phoneNumber, mobile: phoneNumber, }, + status: { account: "normal", }, + app: {}, + }; + newUser.app[appId] = { role: ["user"], wxopenid: openid }; + + user = await DBModel.User.setUser(newUser); + } + } + + // + if (user) { + if (phoneNumber && phoneNumber.length > 0 && user.profile.mobile !== phoneNumber) { + user.profile.mobile = phoneNumber; + } + if (!(appId in user.app)) { + user.app[appId] = { role: ["user"], wxopenid: openid }; + } + user.app[appId].wxopenid = openid; + } else { + return ResponseUtil.internalError(ctx, "用户不存在"); + } + + // 更新Token + const isTokenValid = user.security.token && + user.security.tokenExpiry && + new Date() < user.security.tokenExpiry; + + if (!isTokenValid) { + const token = await this.genToken(user._id.toString()); + user.security.token = token; + } + user.security.tokenExpiry = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000); + await user.save(); + + // 安全起见删除密码相关字段 + delete user.security.passwd; + delete user.security.passwdSalt; + + return ResponseUtil.success(ctx, { user }, "登录成功"); + } catch (err) { + return ResponseUtil.internalError(ctx, err.message); + } + } + + // 获取微信的手机号码 + async wxGetPhoneNumber(ctx) { + try { + const { code, appId } = ctx.request.body; + if (!code || !appId) { + return ResponseUtil.badRequest(ctx, "缺少手机号凭证 code 或 appId"); + } + + let app = config.app[appId]; + if (!app) { + return ResponseUtil.badRequest(ctx, `未配置 appId: ${appId}`); + } + + // 获取access_token + const client_credential_url = `https://api.weixin.qq.com/cgi-bin/token?appid=${app.appid}&secret=${app.secret}&grant_type=client_credential`; + const fetch = (await import("node-fetch")).default; + let sessionRes = await fetch(client_credential_url); + const resp = await sessionRes.json(); + if (!resp.access_token) { + return ResponseUtil.internalError(ctx, "获取微信 access_token 失败"); + } + + // 获取phoneNumber + const phoneUrl = `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${resp.access_token}`; + const phoneRes = await fetch(phoneUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: code }) + }); + const phoneData = await phoneRes.json(); + if (phoneData.errcode) { + return ResponseUtil.error(ctx, `获取手机号失败: ${phoneData.errmsg}`, null, 400); + } + + // 从上图获取手机号 + const phoneNumber = phoneData.phone_info?.phoneNumber; + return ResponseUtil.success(ctx, { phoneNumber }, "获取手机号成功"); + } catch (err) { + return ResponseUtil.internalError(ctx, err.message); + } } } diff --git a/routes/index.js b/routes/index.js index 5f79115..66c02fc 100644 --- a/routes/index.js +++ b/routes/index.js @@ -11,14 +11,16 @@ class ApiRouter { setupRoutes() { const userRouter = new Router({ prefix: '/user' }); - userRouter.post('/wxgetphonenumber', this.handler.wxGetPhoneNumber.bind(this.handler)); - userRouter.post('/wxsignin', this.handler.wxSignin.bind(this.handler)); - userRouter.post('/update', this.handler.updateUser.bind(this.handler)); + userRouter.post('/register', this.handler.register.bind(this.handler)); + userRouter.post('/signin', this.handler.signin.bind(this.handler)); userRouter.post('/signout', this.handler.signout.bind(this.handler)); userRouter.post('/userInfo', this.handler.userInfo.bind(this.handler)); - + userRouter.post('/update', this.handler.updateUser.bind(this.handler)); userRouter.post('/list', this.handler.userList.bind(this.handler)); + userRouter.post('/wxsignin', this.handler.wxSignin.bind(this.handler)); + userRouter.post('/wxgetphonenumber', this.handler.wxGetPhoneNumber.bind(this.handler)); + this.router.use(userRouter.routes()); this.printRoutes(this.router.stack);