From ca8111dab05875e9fee55d4ac46592d9b6998e50 Mon Sep 17 00:00:00 2001 From: mozzie Date: Thu, 16 Mar 2023 16:50:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/.env | 8 +- apps/server/package.json | 6 +- apps/server/src/config/base.config.ts | 2 +- apps/server/src/config/white.api.ts | 1 + apps/server/src/controller/user.controller.ts | 86 ++++--- apps/server/src/dto/sms.dto.ts | 8 + apps/server/src/dto/user.dto.ts | 2 +- apps/server/src/entity/user.entity.ts | 6 +- apps/server/src/service/sms.service.ts | 47 ++++ apps/server/src/service/user.service.ts | 8 +- apps/web/package.json | 3 +- apps/web/public/vip1.svg | 1 + apps/web/public/vip2.svg | 1 + apps/web/public/vip3.svg | 1 + apps/web/src/api/dto.ts | 2 +- apps/web/src/api/index.ts | 3 + apps/web/src/components/Nav/index.less | 8 +- apps/web/src/components/Nav/index.tsx | 102 +++++++- apps/web/src/view/Login/index.less | 3 - apps/web/src/view/Login/index.tsx | 46 ++-- pnpm-lock.yaml | 243 ++++++++++++++---- 21 files changed, 460 insertions(+), 127 deletions(-) create mode 100644 apps/server/src/dto/sms.dto.ts create mode 100644 apps/server/src/service/sms.service.ts create mode 100644 apps/web/public/vip1.svg create mode 100644 apps/web/public/vip2.svg create mode 100644 apps/web/public/vip3.svg diff --git a/apps/server/.env b/apps/server/.env index 0baf97b..e673261 100644 --- a/apps/server/.env +++ b/apps/server/.env @@ -1,8 +1,12 @@ # 代码中使用 process.env.OSS_SECRET OSS_SECRET=12345 - +# 腾讯vod SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5 SUBAPPID=1500018521 -SUBAPPID_OSS=1500018944 \ No newline at end of file +SUBAPPID_OSS=1500018944 + +# 阿里sms +ACCESSKEY_ID=LTAIlHbKxMELdAM0 +ACCESSKEY_SECRET=xLG8tBAMdUG8hVITgZEgl6lAgKamzC \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 7ce1a0b..6b47337 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -26,7 +26,11 @@ "jsonwebtoken": "9.0.0", "tencentcloud-sdk-nodejs": "4.0.552", "vod-node-sdk": "1.1.0", - "chinese-random-name": "2.0.0" + "@alicloud/dysmsapi20170525": "2.0.23", + "@alicloud/openapi-client": "0.4.5", + "@alicloud/tea-util": "1.4.5", + "@alicloud/tea-typescript": "1.8.0", + "object-hash": "3.0.0" }, "devDependencies": { "@midwayjs/cli": "^2.0.0", diff --git a/apps/server/src/config/base.config.ts b/apps/server/src/config/base.config.ts index f64b7f1..7db565a 100644 --- a/apps/server/src/config/base.config.ts +++ b/apps/server/src/config/base.config.ts @@ -4,4 +4,4 @@ export const adminSign = '_sign_admin'; export const adminSignExpired = 60 * 1000 * 10; // 10分钟 export const webSign = '_sign_web'; -export const webSignExpired = 60 * 1000 * 10; // 10分钟 +export const webSignExpired = 60 * 1000 * 100; // 10分钟 diff --git a/apps/server/src/config/white.api.ts b/apps/server/src/config/white.api.ts index d487eb5..f7da476 100644 --- a/apps/server/src/config/white.api.ts +++ b/apps/server/src/config/white.api.ts @@ -3,5 +3,6 @@ import { globalPrefix } from './base.config'; export const whiteApis = [ '/user/admin/auth', '/user/web/auth', + '/user/web/sms', '/course/select/all', ].map(api => globalPrefix + api); diff --git a/apps/server/src/controller/user.controller.ts b/apps/server/src/controller/user.controller.ts index 5bb112a..8da11f7 100644 --- a/apps/server/src/controller/user.controller.ts +++ b/apps/server/src/controller/user.controller.ts @@ -10,7 +10,10 @@ import { import { UserAdminAuthDTO, UserWebAuthDTO } from '../dto/user.dto'; import { XCodeService } from '../service/xcode.service'; import { UserService } from '../service/user.service'; -import { createToken, decodeToken, md5 } from '../util/encrypt'; +import { createToken, decodeToken } from '../util/encrypt'; +import { SmsService } from '../service/sms.service'; +import { SmsDTO } from '../dto/sms.dto'; +import { RedisService } from '@midwayjs/redis'; @Controller('/user') export class UserController { @@ -23,54 +26,44 @@ export class UserController { @Inject() xcodeService: XCodeService; + @Inject() + smsService: SmsService; + + @Inject() + redisService: RedisService; + /** * 用户前台登录 */ @Post('/web/auth') async webAuth(@Body() params: UserWebAuthDTO) { try { + const verifyCode = await this.redisService.get('' + params.user_login); + if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' }; + // 查询用户是否存在 const userExist = await this.userService.select(params); + let payload = {}; if (userExist?.id) { const { user_pass, ...rest } = userExist; - const passValid = userExist.user_pass === md5(params.user_pass); - const token = createToken({ ...rest, hasLogin: true }); - if (passValid) { - this.ctx.cookies.set(webSign, token, { - expires: new Date(Date.now() + webSignExpired), - httpOnly: false, - }); - return { - code: BizCode.OK, - msg: '欢迎回来', - data: { ...rest }, - }; - } else { - return { code: BizCode.ERROR, msg: '密码错误' }; - } + payload = rest; } else { // 新用户注册 - // 1. 验证邀请码 - const { xcode } = params; - const xcodeValid = await this.xcodeService.valid(xcode); - if (!xcodeValid) - return { code: BizCode.ERROR, msg: '新用户注册,请填写神秘代码哦!~' }; - // 2. 创建用户 - const createUser = await this.userService.createUser(params); - const { user_pass, ...rest } = createUser; - // 3. 邀请码表绑定用户 - await this.xcodeService.use(xcode, rest.id); - - const token = createToken({ ...rest, hasLogin: true }); - this.ctx.cookies.set(webSign, token, { - expires: new Date(Date.now() + webSignExpired), - httpOnly: false, - }); - return { - code: BizCode.OK, - data: { ...rest }, - msg: '欢迎来到 backset.cn,' + rest.display_name, - }; + const { user_pass, ...rest } = await this.userService.createUser( + params + ); + payload = rest; } + const token = createToken({ ...payload, hasLogin: true }); + this.ctx.cookies.set(webSign, token, { + expires: new Date(Date.now() + webSignExpired), + httpOnly: false, + }); + await this.redisService.del('' + params.user_login); + return { + code: BizCode.OK, + msg: '欢迎来到 backset.cn', + data: { ...payload }, + }; } catch (error) { this.ctx.logger.error(error); return { code: BizCode.ERROR, msg: '[error] web/auth error' }; @@ -96,7 +89,7 @@ export class UserController { } @Get('/web/state') - async() { + async state() { try { const token = this.ctx.cookies.get(webSign); const user = decodeToken(token); @@ -106,4 +99,21 @@ export class UserController { return { code: BizCode.ERROR, msg: '[error] /web/state error' }; } } + + @Post('/web/sms') + async verifyCode(@Body() params: SmsDTO) { + try { + const { phoneNumber: phoneNumbers } = params; + const code = Math.floor(Math.random() * 9000 + 1000); + await this.redisService.set('' + phoneNumbers, code, 'EX', 60); + console.log('redis here'); + await this.smsService.send({ code, phoneNumbers }); + console.log('sms here'); + return { code: BizCode.OK }; + } catch (error) { + console.log(error); + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: '[error] /web/sms error' }; + } + } } diff --git a/apps/server/src/dto/sms.dto.ts b/apps/server/src/dto/sms.dto.ts new file mode 100644 index 0000000..a65436f --- /dev/null +++ b/apps/server/src/dto/sms.dto.ts @@ -0,0 +1,8 @@ +import { Rule, RuleType } from '@midwayjs/validate'; + +export class SmsDTO { + @Rule( + RuleType.string().required().length(11).error(new Error('手机号格式错误')) + ) + phoneNumber?: string | number; +} diff --git a/apps/server/src/dto/user.dto.ts b/apps/server/src/dto/user.dto.ts index 03ae4c4..83e30e1 100644 --- a/apps/server/src/dto/user.dto.ts +++ b/apps/server/src/dto/user.dto.ts @@ -7,7 +7,7 @@ export class UserWebAuthDTO { ) user_login: string; - @Rule(RuleType.string().required().min(6).error(new Error('密码长度至少6位'))) + @Rule(RuleType.string().required().min(4).error(new Error('验证码4位数字'))) user_pass: string; @Rule(RuleType.string().allow('')) diff --git a/apps/server/src/entity/user.entity.ts b/apps/server/src/entity/user.entity.ts index 383288e..caa8878 100644 --- a/apps/server/src/entity/user.entity.ts +++ b/apps/server/src/entity/user.entity.ts @@ -1,5 +1,4 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; -const randomName = require('chinese-random-name'); @Entity('user') export class User { @@ -21,6 +20,9 @@ export class User { @Column({ default: true }) user_status?: boolean; - @Column({ default: randomName.generate() }) + @Column({ default: '用户' + Date.now() }) display_name?: string; + + @Column({ default: '' }) + user_avatar?: string; } diff --git a/apps/server/src/service/sms.service.ts b/apps/server/src/service/sms.service.ts new file mode 100644 index 0000000..aaac81d --- /dev/null +++ b/apps/server/src/service/sms.service.ts @@ -0,0 +1,47 @@ +// This file is auto-generated, don't edit it +import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525'; +// 依赖的模块可通过下载工程中的模块依赖文件或右上角的获取 SDK 依赖信息查看 +import * as $OpenApi from '@alicloud/openapi-client'; +import Util, * as $Util from '@alicloud/tea-util'; +import { Context, Inject, Provide } from '@midwayjs/core'; + +@Provide() +export class SmsService { + @Inject() + ctx: Context; + + /** + * 使用AK&SK初始化账号Client + * @param accessKeyId + * @param accessKeySecret + * @return Client + * @throws Exception + */ + createClient(accessKeyId: string, accessKeySecret: string): Dysmsapi20170525 { + const config = new $OpenApi.Config({ accessKeyId, accessKeySecret }); + // 访问的域名 + config.endpoint = `dysmsapi.aliyuncs.com`; + return new Dysmsapi20170525(config); + } + + async send({ code, phoneNumbers }): Promise { + // 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html + const client = this.createClient( + process.env['ACCESSKEY_ID'], + process.env['ACCESSKEY_SECRET'] + ); + const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({ + phoneNumbers: phoneNumbers, + signName: '寻鹿网', + templateCode: 'SMS_186510297', + templateParam: `{"code":"${code}"}`, + }); + const runtime = new $Util.RuntimeOptions({}); + try { + // 复制代码运行请自行打印 API 的返回值 + await client.sendSmsWithOptions(sendSmsRequest, runtime); + } catch (error) { + throw new Error(Util.assertAsString(error.message)); + } + } +} diff --git a/apps/server/src/service/user.service.ts b/apps/server/src/service/user.service.ts index 719c101..829975c 100644 --- a/apps/server/src/service/user.service.ts +++ b/apps/server/src/service/user.service.ts @@ -3,14 +3,14 @@ import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { UserWebAuthDTO } from '../dto/user.dto'; import { User } from '../entity/user.entity'; -import { md5 } from '../util/encrypt'; +import hash from 'object-hash'; @Provide() export class UserService { @InjectEntityModel(User) userModel: Repository; - async select(p: UserWebAuthDTO): Promise { + async select(p: UserWebAuthDTO): Promise { const { user_login } = p; const user = await this.userModel.findOne({ where: { user_login }, @@ -19,7 +19,9 @@ export class UserService { } async createUser(user: User) { - user.user_pass = md5(user.user_pass); + const h = hash('' + user.user_login); + user.display_name = h.substring(0, 8); + user.user_avatar = h; const result = await this.userModel.save(user); return result; } diff --git a/apps/web/package.json b/apps/web/package.json index c6912fa..6d88e41 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,7 +17,8 @@ "@ricons/fluent": "0.12.0", "@ricons/utils": "0.1.6", "dplayer": "1.27.1", - "highlight.js": "11.7.0" + "highlight.js": "11.7.0", + "identicon.js": "2.3.3" }, "devDependencies": { "@types/react": "^18.0.27", diff --git a/apps/web/public/vip1.svg b/apps/web/public/vip1.svg new file mode 100644 index 0000000..41e0650 --- /dev/null +++ b/apps/web/public/vip1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/vip2.svg b/apps/web/public/vip2.svg new file mode 100644 index 0000000..4bde6d9 --- /dev/null +++ b/apps/web/public/vip2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/vip3.svg b/apps/web/public/vip3.svg new file mode 100644 index 0000000..49a7263 --- /dev/null +++ b/apps/web/public/vip3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/api/dto.ts b/apps/web/src/api/dto.ts index 949e852..c8cf5b3 100644 --- a/apps/web/src/api/dto.ts +++ b/apps/web/src/api/dto.ts @@ -1,5 +1,5 @@ export interface ILoginRequest { user_login: string; user_pass: string; - xcode: string; + // xcode: string; } diff --git a/apps/web/src/api/index.ts b/apps/web/src/api/index.ts index 93ab0ae..be1d5b1 100644 --- a/apps/web/src/api/index.ts +++ b/apps/web/src/api/index.ts @@ -10,3 +10,6 @@ export const userLogin = (p: ILoginRequest) => R.post("/api/user/web/auth", { ...p }); export const userState = () => R.get("/api/user/web/state"); + +export const sms = (phoneNumber: string | number) => + R.post("/api/user/web/sms", { phoneNumber }); diff --git a/apps/web/src/components/Nav/index.less b/apps/web/src/components/Nav/index.less index de52543..85c5cf5 100644 --- a/apps/web/src/components/Nav/index.less +++ b/apps/web/src/components/Nav/index.less @@ -53,7 +53,7 @@ header { .end { text-align: right; - span { + .btn { cursor: pointer; margin-left: 32px; @@ -61,6 +61,12 @@ header { color: #fff; } } + .user { + font-size: 12px; + > div { + line-height: 1; + } + } } } } diff --git a/apps/web/src/components/Nav/index.tsx b/apps/web/src/components/Nav/index.tsx index cddd448..d3e85e0 100644 --- a/apps/web/src/components/Nav/index.tsx +++ b/apps/web/src/components/Nav/index.tsx @@ -1,14 +1,78 @@ import "./index.less"; import { useNavigate } from "react-router-dom"; import { menuRouters } from "../../router"; -import { Input } from "@arco-design/web-react"; import { useUserStore } from "../../store/user.store"; +import { + Avatar, + Button, + Dropdown, + Link, + Menu, + Space, + Tooltip, +} from "@arco-design/web-react"; +import { + IconDown, + IconImport, + IconSettings, +} from "@arco-design/web-react/icon"; +import Identicon from "identicon.js"; + +const vip1 = ( + + + +); + +const vip2 = ( + + + +); + +const vip3 = ( + + + +); + +const iconStyle = { + marginRight: 8, + fontSize: 16, + transform: "translateY(1px)", +}; function Nav() { const navigate = useNavigate(); const user = useUserStore((s: any) => s.user); const exit = useUserStore((s: any) => s.userExit); + // user.user_avatar = `data:image/png;base64,${new Identicon(h).toString()}`; + + const onClickMenuItem = (key: string) => { + switch (key) { + case "exit": + exit(); + break; + case "setting": + console.log("navigate"); + break; + } + }; + + const dropList = ( + onClickMenuItem(key)}> + + + 退出 + + + + 设置 + + + ); + return (
diff --git a/apps/web/src/view/Login/index.less b/apps/web/src/view/Login/index.less index cc1359e..7990d16 100644 --- a/apps/web/src/view/Login/index.less +++ b/apps/web/src/view/Login/index.less @@ -55,8 +55,5 @@ .sms-group { display: flex; align-items: center; - button { - height: 48px; - } } } diff --git a/apps/web/src/view/Login/index.tsx b/apps/web/src/view/Login/index.tsx index 0d5fc57..b55c468 100644 --- a/apps/web/src/view/Login/index.tsx +++ b/apps/web/src/view/Login/index.tsx @@ -1,17 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import { userLogin } from "../../api"; -import { Message, Button } from "@arco-design/web-react"; +import { sms, userLogin } from "../../api"; +import { Message, Button, Space } from "@arco-design/web-react"; import "./index.less"; import { useNavigate } from "react-router-dom"; import { useUserStore } from "../../store/user.store"; - const defaultForm = { user_login: "", user_pass: "", - xcode: "", }; -const DURATION = 3; // 验证码倒计时 +const DURATION = 60; // 验证码倒计时 export function Login() { const [loginForm, setLoginForm] = useState(defaultForm); @@ -21,10 +19,15 @@ export function Login() { const setUser = useUserStore((s: any) => s.setUser); const onClickSmsBtn = () => { - setTimeout(() => { - Message.success("验证码已发送"); - timer.current = setInterval(() => setCountdown(countdown--), 1000); - }, 500); + if (!/^1[3456789]\d{9}$/.test(loginForm.user_login)) + return Message.error("手机号格式有误"); + sms(loginForm.user_login).then((res: any) => { + if (res?.code === 10000) + setTimeout(() => { + Message.success("验证码已发送"); + timer.current = setInterval(() => setCountdown(countdown--), 1000); + }, 50); + }); }; const onClickLogin = () => { @@ -62,7 +65,7 @@ export function Login() {

欢迎,Backset!

- + /> */} setLoginForm((p) => ({ ...p, @@ -85,7 +88,7 @@ export function Login() { })) } /> - - {/*
+ /> */} +
{ + if (e.key === "Enter") onClickLogin(); + }} onChange={(e) => - setSmsLoginForm((p) => ({ + setLoginForm((p) => ({ ...p, user_pass: e.target.value, })) } placeholder="验证码" /> - -
*/} +