521 Commits

Author SHA1 Message Date
3e23f08eea chore: 不优化首页头像
Some checks failed
Deploy to K3s / deploy (push) Has been cancelled
2026-01-19 14:19:22 +08:00
88a017d6da feat: 优化控制台登陆深色模式表现
All checks were successful
Deploy to K3s / deploy (push) Successful in 1m27s
2026-01-03 15:10:10 +08:00
a718a5487a feat: 博客页支持深色模式 2026-01-03 15:06:17 +08:00
a04227016e chore: 修复首页图像警告 2026-01-03 14:54:45 +08:00
12724bea7f chore: 修复资源卡片图像的警告 2026-01-03 14:53:58 +08:00
720ca56eb3 feat: 添加资源页深色模式支持 2026-01-03 14:51:37 +08:00
8c01303c6c feat: 优化footer深色模式样式 2026-01-03 14:43:01 +08:00
5e2e18fce6 feat: 添加首页、header/footer组件深色模式支持 2026-01-03 14:41:38 +08:00
33053b4a92 secure: 更新nextjs...
All checks were successful
Deploy to K3s / deploy (push) Successful in 3m34s
2026-01-02 23:55:15 +08:00
1c518b44cc chore: 移除自动迁移...
All checks were successful
Deploy to K3s / deploy (push) Successful in 11s
2025-12-27 14:32:01 +08:00
cd80375cc5 chore: ..
Some checks failed
Deploy to K3s / deploy (push) Failing after 5m6s
2025-12-27 14:26:20 +08:00
c23e822cd6 chore: 尝试用job迁移数据库
Some checks failed
Deploy to K3s / deploy (push) Failing after 35s
2025-12-27 14:22:09 +08:00
375d12ab0f lint: 移除前端未使用的import
Some checks failed
Deploy to K3s / deploy (push) Failing after 2m4s
2025-12-27 14:15:05 +08:00
83bdc924b9 chore: 添加数据库迁移
Some checks failed
Deploy to K3s / deploy (push) Failing after 1m49s
2025-12-27 14:11:23 +08:00
c75a67c0d9 chore: 调整bloglist不返回deletedAt字段 2025-12-27 13:53:15 +08:00
0b9963bb29 feat: 调整sitemap支持slug 2025-12-27 13:50:44 +08:00
b48ed4d903 feat: 调整博客页以支持slug 2025-12-27 13:48:48 +08:00
b9d09a16ec feat: 优化博客表格 2025-12-27 13:25:11 +08:00
8c43f5fa73 feat: 编辑博客支持Slug字段,添加复制链接功能 2025-12-27 13:19:17 +08:00
3ea57ba023 chore: 调整复制分享链接命名 2025-12-27 13:14:46 +08:00
a932178509 refactor: 调整复制博客URL到通用函数 2025-12-27 13:13:16 +08:00
2c76d1380f feat: 创建博客时,不允许Slug为空 2025-12-27 13:07:37 +08:00
58b7f592fe feat: 创建博客支持slug字段了 2025-12-27 13:05:07 +08:00
a2e8ddebca feat: 博客添加slug字段 2025-12-27 12:26:58 +08:00
13ec36aa8f lint
All checks were successful
Deploy to K3s / deploy (push) Successful in 2m5s
2025-12-25 15:17:58 +08:00
db8d8c429d feat: 调整博客页获取数据部分为服务端渲染
Some checks failed
Deploy to K3s / deploy (push) Failing after 1m2s
2025-12-25 15:00:06 +08:00
8dc2473a1c feat: 优化handleAPIError函数,返回handler执行结果 2025-12-25 14:54:37 +08:00
616b1ad389 feat: 前端添加robots.ts和sitemap.ts
All checks were successful
Deploy to K3s / deploy (push) Successful in 4m17s
2025-12-24 14:03:34 +08:00
0ef987932f chore: 前端调整博客结构定义 2025-12-24 14:02:03 +08:00
004548c9df feat: 后端博客列表时,添加updatedAt字段 2025-12-24 13:59:30 +08:00
941633bdb4 chore: 移除next-sitemap... 2025-12-24 13:58:58 +08:00
abaa16a0f9 feat: 博客在站内打开 2025-12-24 13:42:56 +08:00
f64b9bb469 chore: 前端添加next-sitemap依赖 2025-12-24 13:42:29 +08:00
f2afe4f7ee fix: 修复首页下元数据错误的问题 2025-12-22 09:03:46 +08:00
dc938fdb01 feat: 优化博客页样式、语义化
All checks were successful
Deploy to K3s / deploy (push) Successful in 2m55s
2025-12-20 23:18:06 +08:00
d7c84ea0ce feat: 优化博客内容标题 描述 2025-12-20 23:09:35 +08:00
4d30605872 feat: 获取博客API添加描述字段 2025-12-20 23:09:03 +08:00
fbc12f97db refactor: 优化控制台、博客、资源语义化 2025-12-20 23:08:30 +08:00
5d62fd89b9 feat: 获取文章详情,返回描述 2025-12-20 23:06:59 +08:00
60d8ad8e8a refactor: 优化首页语义化 2025-12-20 22:39:48 +08:00
ddc9e613e2 chore: 优化后端构建方式
All checks were successful
Deploy to K3s / deploy (push) Successful in 5m55s
2025-12-20 22:08:37 +08:00
93688a0e4e chore: 优化前端构建方式 2025-12-20 22:01:51 +08:00
e0822528a7 chore: 移除数据库迁移CICD...
All checks were successful
Deploy to K3s / deploy (push) Successful in 9s
2025-12-20 21:03:58 +08:00
88bcf06e35 chore: 添加数据库迁移超时打印日志
All checks were successful
Deploy to K3s / deploy (push) Successful in 2m10s
2025-12-20 20:53:50 +08:00
7ac8263b6b fix: 尝试修复backend-migration任务超时
Some checks failed
Deploy to K3s / deploy (push) Failing after 2m5s
2025-12-20 20:05:09 +08:00
d3a7d03be7 feat: 数据库迁移支持
Some checks failed
Deploy to K3s / deploy (push) Failing after 3m52s
2025-12-20 19:58:47 +08:00
59529519e3 chore: 最终测试
All checks were successful
Deploy to K3s / deploy (push) Successful in 35s
2025-12-20 15:16:03 +08:00
2ca6a1ec42 debug: .....
All checks were successful
Deploy to K3s / deploy (push) Successful in 3s
2025-12-20 15:12:38 +08:00
1e3b9faa8b debug: ....
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 15:09:43 +08:00
f3e31106d0 debug: ...
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 15:06:55 +08:00
700a446e77 debug: ..
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:48:11 +08:00
204bcff75c debug: add
Some checks failed
Deploy to K3s / deploy (push) Failing after 3s
2025-12-20 14:43:14 +08:00
35b76b70c9 chore: ..
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:42:16 +08:00
05480cac6b chore: 移除调试信息
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:39:27 +08:00
5c103c4880 test: 修改挂载路径
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:38:53 +08:00
8a174fbed1 test: 手动挂载volumes
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:30:27 +08:00
70b48d1892 test: debug kubeconfig
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 14:27:57 +08:00
9d4607c7cd chore: ..
Some checks failed
Deploy to K3s / deploy (push) Failing after 4s
2025-12-20 13:54:15 +08:00
695577d53a chore: 提交deploy文件,step.run修改为相对路径
Some checks failed
Deploy to K3s / deploy (push) Failing after 16s
2025-12-20 13:26:22 +08:00
4ca1fb5ac9 chore: 标准化头像和网站图标
Some checks failed
Deploy to K3s / deploy (push) Failing after 1m57s
2025-12-20 13:20:18 +08:00
e4d7bc1a3a chore: ...
Some checks failed
Deploy to K3s / deploy (push) Failing after 6m24s
2025-12-20 13:00:25 +08:00
71915f415f chore: deploy.yml....
Some checks failed
Deploy to K3s / deploy (push) Failing after 1s
2025-12-20 12:56:27 +08:00
efc87cdbaf chore: 调整版本为v6
Some checks failed
Deploy to K3s / deploy (push) Failing after 15s
2025-12-20 12:53:53 +08:00
eb21556797 chore: 调整deploy.actions/checkout@v4
Some checks failed
Deploy to K3s / deploy (push) Failing after 16s
2025-12-20 12:52:13 +08:00
b9a03cb167 chore: 提交deploy.yml
Some checks failed
Deploy to K3s / deploy (push) Failing after 16s
2025-12-20 12:48:52 +08:00
4745e2b060 chore: 移除.drone.yml 2025-12-20 11:41:19 +08:00
d7ea4e52cc test: verify drone ci
Some checks are pending
continuous-integration/drone/push Build is pending
2025-12-20 01:26:59 +08:00
e233f0d8bc feat: add custom event trigger
Some checks failed
continuous-integration/drone/push Build is pending
continuous-integration/drone Build was killed
2025-12-20 01:14:07 +08:00
41944f0828 feat: remove runner labels
Some checks are pending
continuous-integration/drone/push Build is pending
2025-12-20 00:59:01 +08:00
da16cf0f04 feat: add runner labels
Some checks failed
continuous-integration/drone/push Build was killed
2025-12-20 00:55:26 +08:00
c8a78aff5d chore: 提交.drone.yml
Some checks failed
continuous-integration/drone/push Build was killed
2025-12-20 00:48:12 +08:00
97f5d8bad1 chore: 依旧修复前端dockerfile 2025-12-20 00:07:47 +08:00
b2bff53beb chore: 修复前端dockerfile启动时API_BASE丢失问题 2025-12-20 00:04:53 +08:00
9a09c09f2c chore: 修改前端生产部署默认端口3000 2025-12-19 23:07:14 +08:00
40ddc4d793 chore: 修复前端dockerfile 2025-12-19 22:51:44 +08:00
1478c6aa71 chore: 修复一个构建时的bug 2025-12-19 22:42:36 +08:00
e90d7fb784 chore: 不知道自己在干嘛... 2025-12-19 22:39:57 +08:00
40f15a37d4 chore: 提交pnpm-lock 2025-12-19 22:37:12 +08:00
0a980bc678 chore: 添加后端nest依赖 2025-12-19 22:34:43 +08:00
fdea8fec96 chore: 提交前后端dockerfile 2025-12-19 22:25:26 +08:00
5ece041672 lint 2025-12-19 22:23:16 +08:00
064f67a2b9 refactor: 优化博客列表语义化 2025-12-19 21:18:42 +08:00
34e01b0eb8 fix: 后端添加博客列表遗漏的描述字段 2025-12-19 21:09:55 +08:00
b0502d4d46 refactor: 重构并修复博客相关API 2025-12-19 21:06:19 +08:00
b69d64f726 chore: 暂时禁用前端短信登陆,并添加一个小彩蛋 2025-12-19 20:56:27 +08:00
05c8fd067b feat: 添加部分API限流规则 2025-12-19 20:32:18 +08:00
3ce02f8b28 feat: 全局filter添加请求过于频繁的错误信息处理 2025-12-19 20:11:59 +08:00
4803145f86 feat: 添加密码登陆接口限流 2025-12-19 20:00:52 +08:00
45d0c87adb feat: 添加默认限流规则名称 2025-12-19 19:56:24 +08:00
7409d1622d refactor: 优化博客评论的登陆用户获取方式,顺手把接口每分钟改成20 2025-12-19 19:38:20 +08:00
acc1e003e8 refactor: 使用CurrentUser替代OSS中用的Req 2025-12-19 19:37:08 +08:00
d8ccbcafc6 feat: admin资源暂时以修改时间倒序 2025-12-19 19:27:23 +08:00
9dc9db2b76 fix: 修复user.findById方法意外暴露了隐私字段的问题 2025-12-19 19:26:24 +08:00
d2e64a70d2 fix: 修复了一堆API错误,并顺手添加了OSS API 2025-12-19 19:12:13 +08:00
7d16d0d9e7 fix: 修复一个拼写错误 2025-12-19 19:11:26 +08:00
ef2fa6fe5c feat: 实现前端资源管理 2025-12-19 19:04:12 +08:00
586a2976d2 refactor: 调整adminResource服务结构 2025-12-19 19:02:10 +08:00
89e99dc9e9 fix: 修复RolesGuard因AuthGuard结构变化导致的不可用的问题 2025-12-19 19:01:00 +08:00
06e1264df1 feat: 添加Common模块 2025-12-19 18:58:36 +08:00
d85982c1d6 feat: 导出前端Admin API 2025-12-19 17:52:47 +08:00
036bed7d23 feat: 前端添加配置管理页 2025-12-19 17:52:04 +08:00
ac0f3bef42 feat: 前端添加admin API 2025-12-19 17:51:45 +08:00
d87469b210 feat: 点击nav-user的账户信息支持跳转对应页 2025-12-19 10:25:26 +08:00
a960a8b07f chore: 暂时移除use-oss-sts的API 2025-12-19 10:23:34 +08:00
83f3d696d6 chore: 移除app-sidebar无用数据 2025-12-19 10:21:57 +08:00
e3a50adea8 feat: 前端现在支持通行证登录辣~ 2025-12-18 22:41:44 +08:00
eddc4b1b76 fix: 后端修复通行证公钥存取问题 2025-12-18 22:41:18 +08:00
15bf790095 fix: 后端修复通行证登录dto类型校验 2025-12-18 22:40:42 +08:00
d8b8a190ec fix: 后端修复通行证登录cookie的path 2025-12-18 22:40:19 +08:00
d323e694ef feat: 添加前端通行证API的返回类型 2025-12-18 22:39:27 +08:00
4356e355fe feat: 添加账户信息、通行证注册功能 2025-12-18 22:04:54 +08:00
45ae85d56e chore: 修复一个通行证拼写错误 2025-12-18 22:00:18 +08:00
13646a1f1b feat: 前端添加通行证API 2025-12-18 22:00:00 +08:00
023097284a chore: 前端添加field组件 2025-12-18 21:52:00 +08:00
f3c3757c3c chore: 前端引入@simplewebauthn/browser 2025-12-18 21:51:36 +08:00
1e2d269ec1 fix: 后端修复passkey注册时challenge不匹配 2025-12-18 21:51:22 +08:00
055dc3972f feat: 前端添加账户信息菜单 2025-12-18 21:46:15 +08:00
7f93a17526 feat: 前端添加GeneralErrorHandler 2025-12-18 21:45:37 +08:00
bc1fdc5b57 refactor: 还有一个组件.. 2025-12-18 19:01:32 +08:00
169a0b00d6 refactor: 将控制台组件移动到控制台... 2025-12-18 18:59:57 +08:00
37d6003eed refactor: 移除user profile对话框 2025-12-18 18:53:21 +08:00
24386bc7bc feat: 控制台主页未登录自动前往登录页 2025-12-18 17:13:24 +08:00
96fe31ed64 feat: 优化Header组件对控制台的跳转逻辑 2025-12-18 17:12:18 +08:00
c625ceb569 feat: 控制台登陆页添加登陆自动跳转 2025-12-18 17:11:23 +08:00
5a4e54c65f feat: 前端对接登出API 2025-12-18 17:10:23 +08:00
10621ecf51 feat: 前端对接设置密码API 2025-12-18 17:10:02 +08:00
90d36d4cfb feat: 添加登出和修改密码的API 2025-12-18 17:09:29 +08:00
fa4a31a6ff secure: 调整设置密码的响应为null 2025-12-18 17:08:23 +08:00
21c010d131 fix: 修复登出实现 2025-12-18 17:06:28 +08:00
6563c783db feat: 全局filter添加对401的处理 2025-12-18 17:06:11 +08:00
cc3b4d4930 feat: 实现通行证注册登录 2025-12-18 15:59:25 +08:00
653abe12cc chore: 引入@simplewebauthn/server包 2025-12-18 15:59:09 +08:00
fec5fa2553 feat: user模块导出User实体 2025-12-18 15:41:08 +08:00
8c8dde5bbb refactor: 调整user/me和user/password获取user的方法 2025-12-18 14:09:21 +08:00
8b42592201 refactor: 移除authGuard注入user时的类型断言 2025-12-18 14:06:49 +08:00
8b53d0573b feat: 添加CurrentUser修饰器 2025-12-18 14:05:51 +08:00
853bd573ad feat: 扩展Express Request一个user属性 2025-12-18 14:05:04 +08:00
6c01f25081 perf: 移除userSession的联合索引 2025-12-18 13:55:16 +08:00
cd1f6116e8 perf: 移除AuthGuard读取用户的操作 2025-12-18 13:51:37 +08:00
91bc9c86fd refactor: 移除user控制器无用引入 2025-12-18 13:50:42 +08:00
5fb106ec26 feat: 添加passkey实体 2025-12-18 12:16:28 +08:00
77b7bf8ab2 refactor: 调整userSession服务及实体至Auth模块下 2025-12-18 12:16:09 +08:00
d6bf4d3cb3 feat: 添加短信发送限流、忽略爬虫、调整默认限流规则 2025-12-18 11:48:40 +08:00
2df5027c0f chore: 调整登陆验证码24小时最多5条 2025-12-18 11:27:16 +08:00
1d5cb319a9 fix: 修复验证过的sms不会作为最新纪录被取出的问题 2025-12-17 23:27:05 +08:00
8fef21c319 fix: sms登陆忘记返回user信息了嘻嘻 2025-12-17 23:23:46 +08:00
4569d6e443 chore: 给phone登陆改成sms登陆,并顺手实现sms登陆 2025-12-17 23:23:14 +08:00
0575f892ef chore: 优化sms.checksms响应、完成sms登陆、给user.create改了个名儿 2025-12-17 23:18:17 +08:00
ca527e997d fix: 后端还有俩文件落下了 2025-12-17 23:02:23 +08:00
e6fad12b30 feat: 前端实现登陆短信验证发送 2025-12-17 23:01:45 +08:00
2ef3507cea feat: 实现短信模块 2025-12-17 23:01:13 +08:00
54acad1671 feat: 后端也添加了写了一半的人机验证模块 2025-12-17 20:31:23 +08:00
c9e49bb769 feat: 添加写了一半的人机验证通用组件 2025-12-17 20:30:46 +08:00
0f0b5f227d refactor: 前端重构控制台用户状态管理 2025-12-17 15:38:06 +08:00
86086a7054 feat: 前端添加user状态管理 2025-12-17 15:36:22 +08:00
f69d79a0ff feat: 添加user/me封装 2025-12-17 15:35:19 +08:00
84a6e0876c fix: 修复密码登陆响应类型定义 2025-12-17 15:34:35 +08:00
83b68b0669 feat: 前端引入zustand 2025-12-17 15:33:55 +08:00
8c2a50127a refactor: 重构后端鉴权方式 2025-12-17 15:33:25 +08:00
fdc8da2308 fix: 修复前端密码登陆api参数校验问题 2025-12-17 13:27:40 +08:00
471fa141ce chore: 移除前端密码登陆页面无用依赖 2025-12-17 13:27:14 +08:00
70bcb8015c refactor: 前端登陆页新实现 2025-12-16 22:56:34 +08:00
1cd663aa0c feat: 前端资源、博客采用新的API实现 2025-12-16 22:54:38 +08:00
11f5360a52 feat: 前端使用环境变量重写api路径host 2025-12-16 22:52:33 +08:00
5ce34c4c95 feat: 重构前端api封装结构 2025-12-16 22:51:27 +08:00
0018b50914 feat: 前端统一user类型 2025-12-16 22:50:27 +08:00
12c84f3dc8 优化LoginHeader组件 2025-12-16 22:49:31 +08:00
70517058ae feat: 后端调整登陆逻辑 2025-12-16 22:48:51 +08:00
b235ca8a6e feat: 后端加入cookie-parser 2025-12-16 22:48:07 +08:00
9730d05aa0 feat: 后端调整用户权限结构为roleItem 2025-12-16 19:58:48 +08:00
6157976029 feat: 统一后端响应格式 2025-12-15 22:24:32 +08:00
e30fe60277 feat: 优化footer组件实现 2025-12-15 14:38:24 +08:00
50877448ab feat: 优化header组件语义化 2025-12-15 14:17:02 +08:00
578e7eeb4b chore: 添加MIT证书 2025-12-14 22:45:48 +08:00
ecd86dd0b7 fix: 调整博客评论工具至客户端组件 2025-12-12 21:21:06 +08:00
e6f3459f81 feat: 调整前端blog列表为服务端渲染模式 2025-12-12 20:48:47 +08:00
7d3a809fa7 fix: 修复常规请求api路径缺失问题 2025-12-12 20:44:33 +08:00
14137c5472 feat: 调整resource为服务端渲染 2025-12-12 18:08:49 +08:00
90f080e9b1 feat: 更新前端package,修复安全漏洞 2025-12-12 17:28:13 +08:00
b89f83291e feat: 优化项目目录结构 2025-12-12 17:25:26 +08:00
ae627d0496 试图修复博客文章的img样式 2025-09-01 17:48:07 +08:00
b5aae0d5b4 lint 2025-06-23 09:32:25 +08:00
3310bd20e9 禁用博客Image优化 2025-06-23 09:26:20 +08:00
ab3ed103db 修复评论时会暴露用户所有字段的问题 2025-06-23 09:25:15 +08:00
524f99ef9d 获取评论时,返回的user信息仅包含必要字段 2025-06-23 09:23:26 +08:00
e4dba6103e 修复评论时返回博客实体的问题 2025-06-23 09:18:35 +08:00
4d660d4495 前端调整评论发布失败的错误消息 2025-06-23 09:15:31 +08:00
f933d37f80 后端修复获取评论失败的bug 2025-06-23 09:15:10 +08:00
dafdfb5459 修复博客实体与评论关系错误 2025-06-23 09:14:28 +08:00
2b4d7e0aa6 前端管理添加允许评论选项 2025-06-23 08:57:39 +08:00
29d3ddc574 对评论相关接口进行权限设定 2025-06-23 08:56:03 +08:00
a62901176e 博客权限添加允许评论项 2025-06-23 08:55:28 +08:00
582f1216ea 漏了俩文件... 2025-06-23 01:40:46 +08:00
9c4432eb8b 修复一个文件大小写错误.. 2025-06-23 01:39:14 +08:00
7971ad2746 .. 2025-06-23 01:38:49 +08:00
660cacbd53 lint 2025-06-23 01:30:02 +08:00
d96c4c9adf 完成链接复制 2025-06-23 01:29:02 +08:00
b68a08e569 不要乱给Image加fill 2025-06-23 01:13:28 +08:00
617602b1a6 修复了几个damn的bug,终于可以用户端访问了 2025-06-23 01:12:25 +08:00
d2a54b062f 完成剩余需求 2025-06-23 00:43:27 +08:00
e9feb1f8ca 完成博客权限修改 2025-06-23 00:07:23 +08:00
ad0a152bd8 优化组件提取实现 2025-06-22 23:34:20 +08:00
873df4afb0 提取blogPermissionCheckBoxs组件 2025-06-22 23:31:55 +08:00
857d73d2ba 完成博客添加时的权限指定 2025-06-22 23:26:12 +08:00
f3193226e7 调整数据库结构、添加博客权限enum 2025-06-22 23:25:38 +08:00
0889225257 lint 2025-06-22 21:17:39 +08:00
a96869f0ee 提交1.0.0的README 2025-06-22 21:06:56 +08:00
0a33687cb4 修复oss中effect重复循环执行的问题 2025-06-22 20:43:38 +08:00
beabbae9ac 调整web到3002端口 2025-06-20 00:48:15 +08:00
5bd11d9c07 修复博客ip错误 2025-06-20 00:42:03 +08:00
35525f61e6 lint 2025-06-19 23:53:28 +08:00
4ae87be385 重构ossStore 2025-06-19 23:07:06 +08:00
69b8967014 lint 2025-06-19 22:03:57 +08:00
538dd3c81e 重构OssStore 2025-06-19 22:03:50 +08:00
3ee6ea924a 标准化博客评论命名 2025-06-19 16:06:09 +08:00
e016c5aaa3 优化博客与博客评论关系 2025-06-19 16:03:27 +08:00
1f1950551e 博客评论限流5/min,添加前端错误处理 2025-06-19 15:42:38 +08:00
91a60e8cf5 userMe处理无token情况,节省资源 2025-06-19 15:24:44 +08:00
d121860b82 添加userMe错误重试判定 2025-06-19 15:20:02 +08:00
ea5d75f495 实现博客评论用户提示 2025-06-19 15:16:55 +08:00
fdea9d16a6 提升userMe到hook中 2025-06-19 15:01:08 +08:00
2fece3e558 添加控制台菜单加载态 2025-06-19 14:55:58 +08:00
304a3073b9 为hook的useSWR添加错误消息 2025-06-19 11:51:23 +08:00
29cea18585 添加邮件限流 2025-06-19 11:41:30 +08:00
de09f0e928 发送验证码、登陆接口限流20/min 2025-06-19 11:27:00 +08:00
c94b4a0e8b 前端禁用发送短信验证码 2025-06-19 11:23:16 +08:00
7adcede1cd 实现邮件验证发送 2025-06-19 11:21:34 +08:00
33636a169f 暂时移除手机登陆方式 2025-06-19 09:36:08 +08:00
20b2bdc43e 创建账户信息组件 2025-06-19 09:30:04 +08:00
00ce4850fa 移动USER_ME_CACHE_KEY到UserApi中、登出移除该缓存 2025-06-19 09:15:03 +08:00
3ac2a164a5 实现权限级菜单、localStorageSWR缓存 2025-06-19 09:03:52 +08:00
00e6ffe12a 前端user/me提升到console/layout 2025-06-18 17:26:50 +08:00
490f0b56cc 后端实现权限验证 2025-06-18 17:10:55 +08:00
a5b8fa49ed 彻底移除权限模块。。。 2025-06-18 16:23:08 +08:00
d2c4d4ba21 彻底移除权限模块 2025-06-18 16:16:46 +08:00
e4ba655552 后端移除所有权限模块 2025-06-18 15:54:13 +08:00
aed1b422ee 修改网站描述 2025-06-18 15:46:30 +08:00
1f03935b8e 修复批量删除object参数错误 2025-06-17 17:01:53 +08:00
b92988ecad 上传文件成功自动从列表删除 2025-06-17 16:58:53 +08:00
1d4a3d1e29 封装ossStore 2025-06-17 16:53:53 +08:00
2f131e50ee 添加登陆接口限流 2025-06-17 09:37:24 +08:00
1de3a3f197 format + lint 2025-06-14 14:12:18 +08:00
95e8f8c648 添加邮件系统page 2025-06-10 21:37:36 +08:00
c7244131cf 调整控制台侧边栏用户管理 2025-06-10 21:21:57 +08:00
03d681b5d3 调整博客、资源表格,允许任意位置换行 2025-06-10 15:37:56 +08:00
af39e65094 下载存储文件在新窗口打开 2025-06-10 15:03:28 +08:00
b6f750f3ef 优化存储界面文件数量提示 2025-06-10 14:59:57 +08:00
cfb1f0d69b 优化存储界面 2025-06-10 14:57:52 +08:00
3e628013b6 修复修改密码导致界面无法点击的问题 2025-06-10 14:53:54 +08:00
e4d5b32f0d 完成ali-oss文件管理 2025-06-10 13:52:35 +08:00
1ac210aa64 ossSts添加缓存 2025-06-08 22:36:14 +08:00
a4fd4bf5dd 后端实现OssSts 2025-06-08 22:24:59 +08:00
81dcbf0cde 完成博客评论添加ip的控制器 2025-06-08 22:10:47 +08:00
3bdf97ec7b 博客评论添加ip及地址 2025-06-08 22:00:02 +08:00
bff23ebf90 博客图片优化鼠标hover样式 2025-06-08 21:50:55 +08:00
ccbbb29267 控制台允许返回首页 2025-06-08 12:00:48 +08:00
e646b20456 实现博客评论 2025-06-07 15:19:43 +08:00
2627c85ec5 实现评论本地更新 2025-06-07 13:49:33 +08:00
96316e3d51 初步完成评论 2025-06-07 03:21:27 +08:00
11add3c1fa 添加邮件模块 2025-05-20 14:11:22 +08:00
c83607d786 移除所有的.DS_Store 2025-05-20 13:48:16 +08:00
2e16ffe42d 实现修改密码,但引入了修改密码后无法点击窗口的bug 2025-05-18 23:18:42 +08:00
0d586f9aae 实现用户注销和删除系统 2025-05-18 22:25:05 +08:00
32026c5673 user List方法包含软删除的用户 2025-05-18 21:47:02 +08:00
34b6677222 删除adminUserService,合并到userService中 2025-05-18 21:44:33 +08:00
94de47a010 对软删除的用户登录进行处理 2025-05-18 21:39:39 +08:00
0fe0f61e38 user服务findOne方法添加withDeleted选项 2025-05-18 21:39:13 +08:00
e940433b52 优化修改用户密码时失败处理 2025-05-18 21:29:17 +08:00
e56bdd71ef 优化用户登录账号删除的情况 2025-05-18 21:28:55 +08:00
ebdfd9dc77 优化Blog管理列表样式 2025-05-18 21:02:53 +08:00
50ede6b1c7 博客文章添加a标签样式 2025-05-18 20:53:42 +08:00
ee428957bd 添加博客成功自动重置表单 2025-05-18 16:04:30 +08:00
47eba8d35e 修复blank拼写错误 2025-05-18 16:02:08 +08:00
3f8e9c27be 实现访问博客计数 2025-05-18 16:01:12 +08:00
a561d729e2 优化加载台和错误 2025-05-18 15:56:54 +08:00
0f1ecb683e 实现iframe等元素的渲染 2025-05-18 15:47:27 +08:00
98f5543865 优化图片预览 2025-05-18 15:38:09 +08:00
95986a8ecf 实现博客内容解析 2025-05-18 15:25:25 +08:00
7c32f2e9a2 修复footer联系我文本换行问题 2025-05-18 15:25:13 +08:00
0cf4e10376 整理依赖 2025-05-18 14:32:15 +08:00
9c0c163321 初步完成渲染博客文章 2025-05-18 14:30:53 +08:00
b1f72f7759 实现blog后端api 2025-05-18 14:30:18 +08:00
7f2530884c 实现blog列表 2025-05-18 14:30:03 +08:00
bd0cba2526 添加base62编码工具 2025-05-18 14:29:50 +08:00
44594bf1b1 完成BlogApi 2025-05-18 14:29:42 +08:00
dccc703ccd 完成博客增删改查 2025-05-16 22:10:21 +08:00
59a68b372b 实现添加博客 2025-05-16 21:58:33 +08:00
65303ac988 添加资源界面添加描述 2025-05-16 21:29:29 +08:00
e9f333fc07 添加资源界面加载态和错误 2025-05-16 17:33:01 +08:00
1bc34688a1 实现登出 2025-05-13 11:58:29 +08:00
6990df3678 修复一个apiError的错误 2025-05-13 11:58:16 +08:00
8e63ecbd06 优化header 2025-05-13 11:35:17 +08:00
d1a9292443 优化控制台user 2025-05-13 11:35:03 +08:00
7d34807edb 添加图片oss域 2025-05-13 11:34:37 +08:00
0489c803f0 完成me接口 2025-05-13 10:53:25 +08:00
8e97636913 修复header导致服务端客户端渲染不一致问题 2025-05-12 22:51:43 +08:00
94276be2b7 调整组件位置 2025-05-12 22:43:24 +08:00
37c422e752 调整用户图标 2025-05-12 22:41:21 +08:00
60acdd5f7b 移除未使用的引用 2025-05-12 22:41:11 +08:00
11df771dbc 优化资源图片质量、添加tagId 2025-05-12 22:40:58 +08:00
71e5022b4a 资源标题样式调整 2025-05-12 22:29:43 +08:00
a0b90c1247 样式小调整 2025-05-12 22:27:27 +08:00
8acacc4e52 优化文字溢出样式 2025-05-12 22:24:23 +08:00
177bafe48b 优化编辑资源标签样式 2025-05-12 22:16:34 +08:00
d2097e0123 实现资源删除管理 2025-05-12 22:13:09 +08:00
3402d263c1 实现resource界面 2025-05-12 21:50:53 +08:00
875dbea8b9 添加编辑资源加载态 2025-05-12 21:34:59 +08:00
f070712823 完成资源CRUD 2025-05-12 21:31:24 +08:00
4367bda08e 完成资源的增加和查询 2025-05-12 20:53:10 +08:00
bf6196afa2 修复前端api漏掉/api路径问题 2025-05-12 16:01:37 +08:00
995178d212 为resourceListAPi添加类型声明 2025-05-12 16:00:23 +08:00
e4e8b694ce 修复文件夹命名错误,添加命名空间 2025-05-12 15:54:06 +08:00
877fa54633 完成博客、资源管理api 2025-05-12 15:50:50 +08:00
b6e31933b3 实现后端webResource CRUD 2025-05-12 15:30:42 +08:00
f2c5b30418 修复创建用户 密码name错误 2025-05-12 14:00:54 +08:00
2730009ac4 调整用户名长度4~32,昵称1~30 2025-05-12 13:56:20 +08:00
ae6919014e 新增自动刷新 2025-05-12 13:52:38 +08:00
38e715b833 调整路径 2025-05-12 13:34:00 +08:00
ce30d9c3ef 优化错误处理表达 2025-05-12 13:30:48 +08:00
2a1709a951 修复错误捕获 2025-05-12 13:30:09 +08:00
74f874109c 优化后端,实现前端添加用户 2025-05-12 13:29:29 +08:00
805901767c 完成修改密码删除用户 2025-05-12 12:47:27 +08:00
fbc9a4f140 修复前端直接修改user的问题 2025-05-12 12:18:14 +08:00
f7f8a3b3e4 调整user实体,去掉id 2025-05-12 12:09:42 +08:00
7a4855d131 完成了一些接口 2025-05-12 12:07:01 +08:00
d8fd52d73e 完成admin-user-update 2025-05-12 11:40:21 +08:00
0d9ff2bfa6 完成admin-user-get 2025-05-12 10:45:56 +08:00
53a0f4456b 完成管理员user-list 2025-05-12 10:27:50 +08:00
ecc6307266 引入俩新组件 2025-05-12 10:27:37 +08:00
5dac312ba2 后端验证服务成功返回true 2025-05-12 10:22:18 +08:00
2440df9a96 完成项目结构搭建 2025-05-10 19:13:50 +08:00
f9b83feea4 移除重复的控制台 2025-05-10 12:18:32 +08:00
8e09da9912 调整前端目录结构 2025-05-10 12:08:04 +08:00
3c7d84165c 完成用户权限管理 2025-05-08 23:13:24 +08:00
7c99ff6045 实现角色权限管理 2025-05-08 22:59:03 +08:00
fba59416ea 实现admin-permission 2025-05-08 22:42:26 +08:00
5d72012a59 实现admin-role 2025-05-08 22:38:50 +08:00
30e1f54a5d 实现用户角色服务 2025-05-08 22:28:25 +08:00
1b73ef6bc9 实现权限管理的服务 2025-05-08 22:26:04 +08:00
ff4c755fc8 实现管理段用户增删改查改密码 2025-05-08 22:07:06 +08:00
887c714e25 验证pipe禁止未定义的字段 2025-05-08 22:06:44 +08:00
6b520924ea 完成管理员获取用户列表 2025-05-07 23:48:40 +08:00
69c40c39aa 完成权限角色守卫 2025-05-07 23:14:57 +08:00
9a705d5b21 加入权限模块、用户模块加入userRole实体 2025-05-07 22:12:18 +08:00
abe5a61697 移除user实体不需要的引用 2025-05-07 22:04:34 +08:00
5719dc1bb5 添加验证码日志 2025-05-07 18:57:12 +08:00
c16d5c8ef4 移除错误引入的服务 2025-05-07 18:33:41 +08:00
a6007ac6dd 添加博客模块 2025-05-07 18:33:13 +08:00
3719efb149 优化资源删除处理 2025-05-07 18:21:50 +08:00
b1fbf062e0 优化资源查询 2025-05-07 18:17:37 +08:00
298272fc70 添加资源模块,添加资源获取接口 2025-05-07 18:14:31 +08:00
4f6c5c8bf8 放弃短信验证了 2025-05-07 17:21:49 +08:00
64ce865caa 实现登录验证 2025-05-07 16:06:46 +08:00
e03e0fb260 完成验证码发送和验证 2025-05-07 16:01:37 +08:00
3437908e67 优化loginDto错误信息 2025-05-07 15:16:06 +08:00
6bf88f5f51 优化表单验证错误处理 2025-05-07 15:14:09 +08:00
e8913250f2 调整logout从get为post 2025-05-07 15:04:36 +08:00
22d05974a6 添加相应成功标准响应 2025-05-07 15:02:56 +08:00
cdd7630feb 实现登出接口 2025-05-07 14:44:07 +08:00
8c54113c8e 完成jwt鉴权 2025-05-07 14:34:49 +08:00
0fca14d99f 加入userSession Service 2025-05-07 13:38:14 +08:00
f2e1da8285 调整user实体🚰 2025-05-07 13:38:01 +08:00
2e77530f66 user模块注册userSession实体 2025-05-07 13:37:27 +08:00
aa26ba86bb 实现auth模块的登录 2025-05-07 13:37:05 +08:00
6fc4dbf57b auth模块加入userSession和jwt 2025-05-07 13:36:51 +08:00
a4a98472cf app加入PassportModule 2025-05-07 13:36:36 +08:00
f3243ef5d2 加入userSession实体 2025-05-07 13:36:26 +08:00
0792046547 调整user实体位置 2025-05-07 13:36:06 +08:00
ca5a070656 加入了一些依赖 2025-05-07 13:35:58 +08:00
8656a03e98 auth模块导入user模块 2025-05-07 00:02:04 +08:00
41fce81a97 user模块导出Service 2025-05-07 00:01:49 +08:00
f325860555 允许userService进行or查询 2025-05-07 00:01:32 +08:00
9111bbdc3b 更新user实体的密码哈希命名 2025-05-07 00:01:13 +08:00
2ae9db43ec 完善auth服务注释 2025-05-06 22:58:47 +08:00
be0dbe89a4 优化登录controller对登录方式的处理 2025-05-06 22:54:38 +08:00
94cc8feda8 完成后端登录dto验证 2025-05-06 22:52:51 +08:00
cab4fdb6e1 完善前端blog types声明 2025-05-06 19:13:20 +08:00
efa5698564 更新登录注册逻辑 2025-05-06 09:25:57 +08:00
1ba3a2a507 移除无用依赖 2025-04-26 22:00:13 +08:00
bb93977396 调整资源组件 2025-04-26 21:59:05 +08:00
36f5bf4445 引入swr用于请求 2025-04-26 21:58:51 +08:00
7913e99902 优化资源样式 2025-04-26 21:11:17 +08:00
f0a4c23be8 lint 2025-04-26 20:42:05 +08:00
b5c548b56a 优化资源样式 2025-04-26 16:18:05 +08:00
4192e436ea 现在才是完成博客,刚刚是资源 2025-04-26 16:13:29 +08:00
9941f64f34 完成博客 2025-04-26 15:05:47 +08:00
cfd4ade804 优化登录页登录方式样式 2025-04-26 13:03:01 +08:00
e6107f3fbe 完善console界面登录样式 2025-04-26 13:00:17 +08:00
dcd04cf476 提交登录页背景图 2025-04-26 12:59:55 +08:00
d82c064018 完善登录page 2025-04-26 12:59:45 +08:00
dd8945abf8 完善matedata 2025-04-26 12:59:36 +08:00
13220bb169 完成登录界面样式 2025-04-25 23:23:46 +08:00
1ef8b3ab0f 优化首页 2025-04-25 22:25:42 +08:00
b64e55886d 引入头像,完成首页 2025-04-25 22:24:37 +08:00
b62eef66b6 优化footer 2025-04-25 21:43:42 +08:00
3c645b8421 实现header、调整路由 2025-04-25 21:42:02 +08:00
a41510b5a9 引入对话框和vaul图标库 2025-04-25 21:41:01 +08:00
02caab0eea 优化footer 2025-04-24 22:47:12 +08:00
518c26f372 添加默认背景颜色 2025-04-24 22:45:19 +08:00
75164c326b 优化header 2025-04-24 22:43:21 +08:00
6504740d89 添加基础结构 2025-04-24 22:40:18 +08:00
52cf68b829 加入依赖 2025-04-24 22:40:05 +08:00
dd943cbda7 初步完成header和footor 2025-04-24 22:39:53 +08:00
c99b76e7a9 使用shadcn重构 2025-04-24 19:37:31 +08:00
a55726c724 提交nextjs新项目 2025-04-24 17:46:11 +08:00
32851ca569 更新gitignore文件 2025-04-24 17:44:22 +08:00
f1b0eaa9c7 logo也先不要了 2025-04-18 22:00:41 +08:00
f85ee7a8f3 重构项目 2025-04-17 16:11:44 +08:00
5d5a859568 优化Blog样式 2025-03-04 23:20:53 +08:00
9d486db6a3 修复user表作用域问题 2025-02-16 23:51:56 +08:00
ae105a62e9 移除无用的时间函数 2025-02-16 23:40:29 +08:00
42d6179680 修改后端启动方式 2025-02-16 23:28:51 +08:00
b51605cec9 修改后段博客内容时间字段 2025-02-16 23:23:49 +08:00
4fdeaf287b 修复博客内容时间字段问题 2025-02-16 23:16:54 +08:00
c5b2d5ab4a 移除无用的依赖 2025-02-16 23:09:00 +08:00
a3c122e76b 使用pg数据库重构 2025-02-16 23:08:25 +08:00
0b67c146a8 前端首页置标题 2025-01-17 12:42:44 +08:00
9068f76585 修复 前端Header菜单文本换行问题 2024-10-25 21:52:54 +08:00
1da6f8624e 优化 前端博客内容页面样式,调整最小宽度为700px 2024-10-16 22:49:19 +08:00
9840e08066 优化 前端使用协议样式 2024-10-16 22:44:25 +08:00
107b087bff 优化 前端博客列表中描述样式 2024-10-16 22:29:16 +08:00
ec82a21803 修复 后端获取博客内容后 访问次数不+1的情况 2024-10-16 22:21:08 +08:00
b32d344202 优化 前端博客内容空提示 2024-10-16 22:17:22 +08:00
8e6f25d8b3 添加 前端博客列表加密文章提示 2024-10-16 22:10:01 +08:00
a11be6704d 添加 服务端查询博客列表时返回access_level字段 2024-10-16 21:54:03 +08:00
b810fb481f 添加 自动刷新Token功能以维持登录状态 2024-10-16 21:49:04 +08:00
42e8373c21 添加 服务端API中RequestData数据类型定义 2024-10-16 21:48:14 +08:00
7c5b9234cb 修复 前端BlogContent a标签深色模式的样式 2024-10-16 01:50:21 +08:00
25bb2eb0b5 添加 前端BlogContent对表格的样式支持 2024-10-16 01:41:27 +08:00
44a7938e66 添加 加密博客文章功能 2024-10-16 01:35:41 +08:00
007d574eec 优化 前端控制台界面样式 2024-10-15 23:43:05 +08:00
8a73f3cf84 优化 前端Dashboard样式实现 2024-10-14 10:37:53 +08:00
c83b5ab6c0 优化 前端深色模式样式 2024-10-13 14:31:24 +08:00
90183f1a2a 修复 elementplus深色模式不能实时变化的问题 2024-10-13 14:18:36 +08:00
6a7782544b 优化 前端BlogContent 2024-10-12 12:27:25 +08:00
2195af92b6 优化 前端深色模式样式 2024-10-12 11:43:46 +08:00
cf29c35266 添加 ElementPlus深色模式 2024-10-12 11:02:49 +08:00
bdc4b459e2 优化 前端Login登录按钮样式实现 2024-10-12 11:00:57 +08:00
d2c2b6e7ad 修复 前端NotFound组件销毁标题不重置的问题 2024-10-12 11:00:18 +08:00
d005336711 优化 前端NotFound界面 2024-10-12 10:54:32 +08:00
b670e0dcea 优化 文件上传后提示逻辑 2024-10-08 15:27:00 +08:00
87af3766d9 添加 文件上传失败原因提示 2024-10-08 12:57:52 +08:00
2e191c7a7b 添加 深色模式 2024-10-08 02:43:17 +08:00
5038c3b29c 优化 Ace编辑器对语言的支持 2024-10-06 00:38:19 +08:00
9acf2ae81b 添加 OSS文件在线编辑功能package依赖 2024-10-06 00:04:43 +08:00
373a25ec9f 添加 OSS文件在线编辑功能 2024-10-06 00:04:21 +08:00
2179be0b95 优化 OSS文件上传 文件切片、下载文件逻辑、文件操作界面逻辑 2024-10-05 22:37:04 +08:00
1deb720191 添加 前端 OSS文件管理 2024-10-05 01:32:06 +08:00
5b60754ecc 添加 服务端GetOSSToken接口 2024-10-05 01:30:59 +08:00
e826b1c9e0 优化 Resources管理界面 2024-10-01 16:39:32 +08:00
c0902148d8 移除 tonecn中的pnpm-lock 2024-09-30 12:52:44 +08:00
391596c750 gitignore加入pnpm-lock 2024-09-30 12:50:49 +08:00
515c640a10 优化 Resource Download中标签样式 2024-09-30 00:19:43 +08:00
6e32943792 重构 Footer tailwind 2024-09-30 00:17:18 +08:00
ad02d8c221 重构 Rotation tailwind 2024-09-29 23:22:04 +08:00
f87839d0e5 重构 Login tailwind 2024-09-29 23:05:34 +08:00
6ef31e7b3a 重构 BlogContent tailwind 2024-09-29 22:51:17 +08:00
4eb427108e 重构 Blog tailwindcss 2024-09-29 22:18:17 +08:00
4fcf7b8bbe 修复 Header 菜单下边框选中样式层叠失败的问题 2024-09-29 20:40:20 +08:00
0333ba54c0 重构 Download tailwind 2024-09-29 19:36:49 +08:00
fa88148a9a 重构 Resource tailwind 2024-09-29 19:36:41 +08:00
5ed59d1da8 重构 HomeView tailwind 2024-09-29 01:18:17 +08:00
a1736ca390 重构 Header.vue-tailwind 2024-09-29 00:55:06 +08:00
e5ff18abe6 添加 tailwindcss 2024-09-28 22:27:23 +08:00
a53973433f 优化 Logger日志颜色显示 2024-09-13 17:36:51 +08:00
4d61b7df77 优化 login控制台响应式布局 2024-09-12 22:00:55 +08:00
25d21e4fb3 优化 notfound界面响应式布局 2024-09-12 21:27:37 +08:00
20a8b6335f 优化 blogContent加载完成前工具栏出现的问题 2024-09-12 20:32:31 +08:00
a07ce3578b 添加404界面,优化homeview代码可读性 2024-09-12 20:29:45 +08:00
51db80bcd4 登录时按下回车键触发登录 2024-09-06 16:10:51 +08:00
2248b60e2d 修复typescript类型错误 2024-09-06 16:10:46 +08:00
01a049ef9e 前端加入request中间件,jwtToken失效自动清除localstorage及刷新界面 2024-09-06 15:38:55 +08:00
0e47d29a0e 优化captchaSession 2024-09-06 12:00:07 +08:00
d3476ed419 修改APILoader以保持API统一 2024-09-06 11:59:04 +08:00
42cd5015a8 为API抽象类添加注解和类型限定 2024-09-06 11:58:24 +08:00
2e98682e7e 优化MySQL连接池 2024-09-06 11:57:45 +08:00
55c780cc57 删除无用配置文件 2024-09-06 11:57:15 +08:00
4fd9252a2c 前端添加网站图标及优化标签页标题 2024-09-01 16:42:22 +08:00
8515b0ce71 更改IPlocationAPI查询接口 2024-09-01 16:41:35 +08:00
6904136229 自动导入element-plug 2024-09-01 15:49:10 +08:00
ca19f8c9fa 修复路由守卫next多次调用的问题 2024-09-01 15:47:33 +08:00
73c334494f 博客评论后重加载评论 2024-09-01 15:47:10 +08:00
bc062a597b 后端添加tonesc.cn跨域 2024-09-01 15:46:51 +08:00
3a0fc9bae0 前端添加实用工具 2024-09-01 15:28:34 +08:00
de33554099 前端完成Blogs管理界面 2024-09-01 14:37:47 +08:00
c848feaaa8 后端完成博客管理接口 2024-09-01 14:37:21 +08:00
0bc7eeb3ea 后端实现Resources修改接口 2024-09-01 13:46:10 +08:00
bb23f0073d Dashboard完成、Resources编辑完成 2024-09-01 13:45:41 +08:00
a29ee81b54 request自动添加jwtToken 2024-09-01 13:44:59 +08:00
0ca188e50d cors允许DELETE方法 2024-09-01 13:44:46 +08:00
b2cf669b29 优化博客评论显示 2024-08-31 23:43:57 +08:00
19f949090a 优化Blog容器高度 2024-08-31 22:57:45 +08:00
3cdf800cc4 修复因棒棒糖导致的Header高度不统一情况 2024-08-31 22:56:07 +08:00
8ac72de3fe 修复Header中控制台RouterLink指向 2024-08-31 22:53:33 +08:00
e0c9b5d67c 前端登录功能补充 2024-08-31 22:53:02 +08:00
c961cb0508 前端完成登录功能 2024-08-31 22:52:44 +08:00
b3333799cd 后端完成jwt登录接口 2024-08-31 22:52:31 +08:00
a4322aaaed 修复博客内容样式 2024-08-31 16:24:36 +08:00
4496181da0 优化博客访问量显示 2024-08-31 15:05:16 +08:00
bf9ee5c81c 将博客评论组件加入到博客内容中 2024-08-31 14:57:30 +08:00
0bdfbbe001 完成前端博客评论展示组件 2024-08-31 14:57:10 +08:00
83fe38a1b5 新增获取博客评论接口 2024-08-31 14:50:53 +08:00
61c7ec8d2e 修复提交博客评论未进行人机验证检验的bug 2024-08-31 14:50:13 +08:00
8190f5f55c 完善博客评论功能 2024-08-31 13:38:22 +08:00
e9a8ad6717 后端加入Redis、旋转图片验证接口 2024-08-30 21:34:24 +08:00
41b7a38669 前端加入旋转图像验证组件 2024-08-30 21:33:43 +08:00
39cd4bc66b 优化MysqlConnection日志 2024-08-30 21:33:16 +08:00
cdee1205c2 加入BlogContent工具栏,完成点赞功能 2024-08-30 16:28:25 +08:00
76fa835459 添加访问博客次数显示 2024-08-30 16:07:00 +08:00
58fdcc9bc7 BlogList接口返回访问量及点赞量 2024-08-30 12:54:22 +08:00
9f5d385e02 添加pnpm-lock 到gitignore 2024-08-30 12:47:30 +08:00
80258a829c 完成前端BlogContent界面 2024-08-30 12:46:58 +08:00
852d849b69 完善BlogContent接口,完善ServerDtdResponse 2024-08-30 12:46:35 +08:00
a4953bafb3 后端移除AuthAdmin中间件 2024-08-30 12:46:07 +08:00
50f01ec49d 修复Header组件中博客文章和列表显示的问题 2024-08-30 12:45:37 +08:00
2dce1b63be 完成前端Blog界面 2024-08-30 01:09:05 +08:00
325334e849 加入时间戳转字符串库 2024-08-30 01:08:44 +08:00
2addf999fb 修改主页描述 2024-08-29 22:21:43 +08:00
06aa147271 完善资源/下载页面 2024-08-29 22:21:32 +08:00
b9266bfc4f 修复request封装返回数据结构 2024-08-29 22:02:57 +08:00
827fb4169e 重构后端,完善获取资源/下载列表、博客列表接口 2024-08-29 21:59:55 +08:00
298 changed files with 28704 additions and 4849 deletions

View File

@@ -0,0 +1,80 @@
# .gitea/workflows/deploy.yml
name: Deploy to K3s
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: localhost:5000/tiny-ci-runner:latest
env:
IMAGE_TAG: ${{ github.sha }}
KUBECONFIG: /tmp/.kube/config
NODE_ENV: production
steps:
- name: Write kubeconfig
run: |
mkdir -p /tmp/.kube
cat << 'EOF' > /tmp/.kube/config
${{ secrets.KUBECONFIG_DATA }}
EOF
chmod 600 /tmp/.kube/config
- name: Verify Kubernetes access
run: |
kubectl cluster-info
kubectl get nodes
- name: Checkout code
run: |
git clone --depth=1 --branch master \
https://git.tonesc.cn/tone/tonePage.git \
/workspace/tone/tonePage
cd /workspace/tone/tonePage
git log -1 --oneline
- name: Build and push backend image
run: |
cd /workspace/tone/tonePage/apps/backend
docker build -t localhost:5000/backend:${IMAGE_TAG} .
docker push localhost:5000/backend:${IMAGE_TAG}
- name: Build and push frontend image
run: |
cd /workspace/tone/tonePage/apps/frontend
docker build \
--build-arg API_BASE="http://backend-service:3001" \
-t localhost:5000/frontend:${IMAGE_TAG} .
docker push localhost:5000/frontend:${IMAGE_TAG}
- name: Deploy to K3s
run: |
cd /workspace/tone/tonePage/apps/deploy
# 基础资源
kubectl apply -f postgres-deployment.yaml
kubectl apply -f backend-deployment.yaml
kubectl apply -f frontend-deployment.yaml
# 更新镜像(触发滚动更新)
kubectl set image deployment/backend \
backend=localhost:5000/backend:${IMAGE_TAG}
kubectl set image deployment/frontend \
frontend=localhost:5000/frontend:${IMAGE_TAG}
# 等待滚动完成
kubectl rollout status deployment/backend --timeout=120s
kubectl rollout status deployment/frontend --timeout=120s
- name: Post-deploy sanity check
run: |
kubectl get pods
kubectl get svc

30
.gitignore vendored
View File

@@ -1,31 +1 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
pnpm-lock.yaml

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 tonecn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,48 @@
# TONE_Page 个人博客
## 简介
一款由NextJS+NustJS(+Postgres)打造的现代化个人博客平台
## 功能特性
- 资源/工具发布
- 博客发布
- 博客评论及回复
- 用户系统(支持账号密码登录、邮箱验证码登录)
## 安装与运行
```bash
git clone https://git.tonesc.cn/tone/tonePage.git
# 后端
cd tone-page-server
touch .env # 创建并编辑环境变量,需要包含以下信息
npm run build
npm run start:prod
# 前端
cd tone-page-web
npm run build
npm run start
```
```bash
# 后端环境变量
DATABASE_HOST= # 数据库地址Postgres
DATABASE_PORT= # 数据库端口
DATABASE_NAME= # 数据库名称
DATABASE_USERNAME= # 数据库用户名
DATABASE_PASSWORD= # 数据库密码
JWT_SECRET= # JWT密钥任意均可
JWT_EXPIRES_IN= # JWT过期时间例如1d、12h
ALIYUN_ACCESS_KEY_ID= # 阿里云RAM用户ACCESS_KEY_ID
ALIYUN_ACCESS_KEY_SECRET= # 阿里云RAM用户ACCESS_KEY_SECRET
ALIYUN_OSS_STS_ROLE_ARN= # 阿里云OSS_STS需要扮演的角色ARN
NODE_ENV=production # 保留该行表示在生产环境
```
## 注意事项
* 注意后端在正式进入生产环境前,需要先注释```NODE_ENV=production```以实现数据表结构初始化,完成后重启服务,并取消注释即可正式进入生产环境
* 若需使用pm2进行服务管理可通过```pm2 start "npm run start" --name "name"```启动
* 前端服务开放在3002端口后端服务开放在3001端口
## 许可证
MIT

View File

@@ -1,8 +0,0 @@
{
"mysql":{
"host":"server.tonesc.cn",
"user":"root",
"password":"245565",
"database":"tonecn"
}
}

View File

View File

@@ -1,8 +0,0 @@
{
"watch": ["*.ts"],
"execMap": {
"ts": "ts-node"
},
"ignore": ["*.test.ts"],
"ext": "ts"
}

View File

@@ -1,25 +0,0 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.12.12",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"ioredis": "^5.4.1",
"mysql2": "^3.9.7"
}
}

944
Server/pnpm-lock.yaml generated
View File

@@ -1,944 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
cors:
specifier: ^2.8.5
version: 2.8.5
express:
specifier: ^4.19.2
version: 4.19.2
ioredis:
specifier: ^5.4.1
version: 5.4.1
mysql2:
specifier: ^3.9.7
version: 3.9.7
devDependencies:
'@types/node':
specifier: ^20.12.12
version: 20.12.12
nodemon:
specifier: ^3.1.0
version: 3.1.0
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.12)(typescript@5.4.5)
typescript:
specifier: ^5.4.5
version: 5.4.5
packages:
/@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@ioredis/commands@1.2.0:
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
dev: false
/@jridgewell/resolve-uri@3.1.2:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true
/@jridgewell/trace-mapping@0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@tsconfig/node10@1.0.11:
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
dev: true
/@tsconfig/node12@1.0.11:
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
dev: true
/@tsconfig/node14@1.0.3:
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
dev: true
/@tsconfig/node16@1.0.4:
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
dev: true
/@types/node@20.12.12:
resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==}
dependencies:
undici-types: 5.26.5
dev: true
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
dev: false
/acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
dev: true
/acorn@8.11.3:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: true
/arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
/array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
dev: true
/body-parser@1.20.2:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: true
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.4
set-function-length: 1.2.2
dev: false
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: false
/content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
dev: false
/cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false
/cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
dev: false
/cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
dependencies:
object-assign: 4.1.1
vary: 1.1.2
dev: false
/create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: false
/debug@4.3.4(supports-color@5.5.0):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
supports-color: 5.5.0
/define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
es-errors: 1.3.0
gopd: 1.0.1
dev: false
/denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dev: false
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: false
/destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
dev: true
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
/encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: false
/es-define-property@1.0.0:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
dependencies:
get-intrinsic: 1.2.4
dev: false
/es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
dev: false
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: false
/express@4.19.2:
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.2
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.6.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
proxy-addr: 2.0.7
qs: 6.11.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: true
/finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: false
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: false
/generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
dependencies:
is-property: 1.0.2
dev: false
/get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
has-proto: 1.0.3
has-symbols: 1.0.3
hasown: 2.0.2
dev: false
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: true
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
get-intrinsic: 1.2.4
dev: false
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
/has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
dependencies:
es-define-property: 1.0.0
dev: false
/has-proto@1.0.3:
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
engines: {node: '>= 0.4'}
dev: false
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
dependencies:
function-bind: 1.1.2
dev: false
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
dev: true
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/ioredis@5.4.1:
resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
engines: {node: '>=12.22.0'}
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.4(supports-color@5.5.0)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
dev: false
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
dev: false
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.3.0
dev: true
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: true
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: true
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: true
/is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
dev: false
/lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
dev: false
/lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
dev: false
/long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
dev: false
/lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
dev: false
/lru-cache@8.0.5:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'}
dev: false
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: true
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: false
/merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: false
/methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: true
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/mysql2@3.9.7:
resolution: {integrity: sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==}
engines: {node: '>= 8.0'}
dependencies:
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.6.3
long: 5.2.3
lru-cache: 8.0.5
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
dev: false
/named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
dependencies:
lru-cache: 7.18.3
dev: false
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/nodemon@3.1.0:
resolution: {integrity: sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
chokidar: 3.6.0
debug: 4.3.4(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.6.2
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
dev: true
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: false
/object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: false
/on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: false
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
dev: false
/pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
dev: true
/qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.6
dev: false
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: false
/raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: true
/redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
dev: false
/redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
dependencies:
redis-errors: 1.2.0
dev: false
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/semver@7.6.2:
resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==}
engines: {node: '>=10'}
hasBin: true
dev: true
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
dev: false
/serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 1.0.2
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
dev: false
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
dependencies:
define-data-property: 1.1.4
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.4
gopd: 1.0.1
has-property-descriptors: 1.0.2
dev: false
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
get-intrinsic: 1.2.4
object-inspect: 1.13.1
dev: false
/simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
dependencies:
semver: 7.6.2
dev: true
/sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
dev: false
/standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
dev: false
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: true
/toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
dev: true
/ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5):
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 20.12.12
acorn: 8.11.3
acorn-walk: 8.3.2
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.4.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
dev: false
/typescript@5.4.5:
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
dev: true
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: false
/utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: false
/v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: false
/yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
dev: true

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@plugs/*": ["plugs/*"],
"@apis/*": ["apis/*"]
}
}
}

4
apps/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
.next
.git
.env.local

25
apps/backend/.eslintrc.js Normal file
View File

@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

56
apps/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
apps/backend/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

23
apps/backend/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM node:22-alpine AS builder
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
RUN CI=true pnpm prune --prod
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
EXPOSE 3001
CMD ["node", "dist/main.js"]

99
apps/backend/README.md Normal file
View File

@@ -0,0 +1,99 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ pnpm install
```
## Compile and run the project
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Run tests
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ pnpm install -g mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

94
apps/backend/package.json Normal file
View File

@@ -0,0 +1,94 @@
{
"name": "tone-page-server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:generate": "typeorm migration:generate -d src/data-source.ts",
"migration:run": "typeorm migration:run -d dist/data-source.js",
"migration:revert": "typeorm migration:revert -d dist/data-source.js"
},
"dependencies": {
"@alicloud/credentials": "^2.4.3",
"@alicloud/dm20151123": "1.2.6",
"@alicloud/dypnsapi20170525": "^2.0.0",
"@alicloud/dysmsapi20170525": "4.1.0",
"@alicloud/openapi-client": "^0.4.14",
"@alicloud/tea-util": "^1.4.10",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0",
"@simplewebauthn/server": "^13.2.2",
"@types/ali-oss": "^6.16.11",
"ali-oss": "^6.23.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.2",
"pg": "^8.15.6",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.22",
"uuid": "^11.1.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

7383
apps/backend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminController } from './admin.controller';
describe('AdminController', () => {
let controller: AdminController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminController],
}).compile();
controller = module.get<AdminController>(AdminController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('admin')
export class AdminController {}

View File

@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminUserController } from './controller/admin-user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/user/entities/user.entity';
import { UserModule } from 'src/user/user.module';
import { AdminWebResourceController } from './controller/web/admin-web-resource.controller';
import { AdminWebBlogController } from './controller/web/admin-web-blog.controller';
import { ResourceModule } from 'src/resource/resource.module';
import { BlogModule } from 'src/blog/blog.module';
import { AuthModule } from 'src/auth/auth.module';
import { AdminResourceService } from './services/admin.resource.service';
@Module({
providers: [
AdminResourceService,
],
imports: [
TypeOrmModule.forFeature([User]),
UserModule,
ResourceModule,
BlogModule,
AuthModule,
],
controllers: [
AdminController,
AdminUserController,
AdminWebResourceController,
AdminWebBlogController,
],
})
export class AdminModule {}

View File

@@ -0,0 +1,83 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ListDto } from '../dto/admin-user/list.dto';
import { CreateDto } from '../dto/admin-user/create.dto';
import { UserService } from 'src/user/user.service';
import { UpdateDto } from '../dto/admin-user/update.dto';
import { UpdatePasswordDto } from '../dto/admin-user/update-password.dto';
import { RemoveUserDto } from '../dto/admin-user/remove.dto';
import { RolesGuard } from 'src/common/guard/roles.guard';
import { Roles } from 'src/common/decorators/role.decorator';
import { Role } from 'src/auth/role.enum';
import { AuthGuard } from 'src/auth/guards/auth.guard';
@Controller('admin/user')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.Admin)
export class AdminUserController {
constructor(private readonly userService: UserService) { }
@Get()
async list(@Query() listDto: ListDto) {
return this.userService.list(listDto.page, listDto.pageSize);
}
@Get(':userId')
async get(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
) {
return this.userService.findOne({ userId });
}
@Post()
async create(@Body() createDto: CreateDto) {
return this.userService.register({
...createDto,
...(createDto.password &&
(() => {
const salt = this.userService.generateSalt();
return {
salt,
password_hash: this.userService.hashPassword(
createDto.password,
salt,
),
};
})()),
});
}
@Put(':userId')
async update(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() updateDto: UpdateDto,
) {
return this.userService.update(userId, updateDto);
}
@Delete(':userId')
async delete(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Query() dto: RemoveUserDto,
) {
return this.userService.delete(userId, dto.soft);
}
@Post(':userId/password')
async setPassword(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() updatePasswordDto: UpdatePasswordDto,
) {
return this.userService.setPassword(userId, updatePasswordDto.password);
}
}

View File

@@ -0,0 +1,64 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { CreateBlogDto } from 'src/admin/dto/admin-web/create-blog.dto';
import { SetBlogPasswordDto } from 'src/admin/dto/admin-web/set-blog-password.dto';
import { UpdateBlogDto } from 'src/admin/dto/admin-web/update-blog.dto';
import { AuthGuard } from 'src/auth/guards/auth.guard';
import { Role } from 'src/auth/role.enum';
import { BlogService } from 'src/blog/blog.service';
import { Roles } from 'src/common/decorators/role.decorator';
import { RolesGuard } from 'src/common/guard/roles.guard';
@Controller('/admin/web/blog')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.Admin)
export class AdminWebBlogController {
constructor(private readonly adminWebBlogService: BlogService) { }
@Get()
async list() {
return this.adminWebBlogService.list({
withAll: true,
});
}
@Post()
async create(@Body() dto: CreateBlogDto) {
return this.adminWebBlogService.create(dto);
}
@Put(':id')
async update(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: UpdateBlogDto,
) {
return this.adminWebBlogService.update(id, dto);
}
@Post(':id/password')
async setPassword(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: SetBlogPasswordDto,
) {
return this.adminWebBlogService.setPassword(id, dto.password);
}
@Get(':id')
async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.adminWebBlogService.findById(id);
}
@Delete(':id')
async remove(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.adminWebBlogService.remove(id);
}
}

View File

@@ -0,0 +1,56 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { CreateResourceDto } from 'src/admin/dto/admin-web/create-resource.dto';
import { AdminResourceService } from 'src/admin/services/admin.resource.service';
import { AuthGuard } from 'src/auth/guards/auth.guard';
import { Role } from 'src/auth/role.enum';
import { Roles } from 'src/common/decorators/role.decorator';
import { RolesGuard } from 'src/common/guard/roles.guard';
@Controller('/admin/web/resource')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.Admin)
export class AdminWebResourceController {
constructor(private readonly resourceService: AdminResourceService) { }
@Get()
async list() {
return this.resourceService.findAll();
}
@Get(':id')
async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.resourceService.findById(id);
}
@Post()
async create(@Body() data: CreateResourceDto) {
return this.resourceService.create(data);
}
@Put(':id')
async update(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() data: CreateResourceDto,
) {
return this.resourceService.update({
...data,
id,
});
}
@Delete(':id')
async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.resourceService.delete(id);
}
}

View File

@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class CreatePermissionDto {
@IsString()
name: string;
@IsString()
description: string;
}

View File

@@ -0,0 +1,8 @@
import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class DeleteRolePermissionsDto {
@IsArray()
@ArrayMinSize(1)
@IsUUID('4', { each: true })
permissionIds: string[];
}

View File

@@ -0,0 +1,8 @@
import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class SetRolePermissionsDto {
@IsArray()
@ArrayMinSize(1)
@IsUUID('4', { each: true })
permissionIds: string[];
}

View File

@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class CreateRoleDto {
@IsString()
name: string;
@IsString()
localName: string;
}

View File

@@ -0,0 +1,13 @@
import { IsBoolean, IsDateString, IsOptional, IsUUID } from 'class-validator';
export class CreateUserRoleDto {
@IsUUID('4')
roleId: string;
@IsBoolean()
isEnabled: boolean;
@IsOptional()
@IsDateString()
expiredAt?: Date;
}

View File

@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator';
export class DeleteUserRoleDto {
@IsUUID('4')
roleId: string;
}

View File

@@ -0,0 +1,32 @@
import { IsString, Length, Matches, ValidateIf } from 'class-validator';
export class CreateDto {
@ValidateIf((o) => o.username !== null)
@IsString({ message: '用户名不得为空' })
@Length(4, 32, { message: '用户名长度只能为4~32' })
username: string | null;
@ValidateIf((o) => o.nickname !== null)
@IsString({ message: '昵称不得为空' })
@Length(1, 30, { message: '昵称长度只能为1~30' })
nickname: string | null;
@ValidateIf((o) => o.email !== null)
@IsString({ message: '邮箱不得为空' })
@Length(6, 254, { message: '邮箱长度只能为6~254' })
email: string | null;
@ValidateIf((o) => o.phone !== null)
@IsString({ message: '手机号不得为空' })
@Length(11, 11, { message: '手机号长度只能为11' })
phone: string | null;
@ValidateIf((o) => o.password !== null)
@IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(
/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
{ message: '密码必须包含字母和数字且长度在6~32之间' },
)
password: string | null;
}

View File

@@ -0,0 +1,3 @@
import { PaginationDto } from '../common/pagination.dto';
export class ListDto extends PaginationDto {}

View File

@@ -0,0 +1,8 @@
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class RemoveUserDto {
@Transform(({ value }) => value === 'true')
@IsBoolean({ message: '需指定删除类型' })
soft: boolean;
}

View File

@@ -0,0 +1,11 @@
import { IsString, Length, Matches } from 'class-validator';
export class UpdatePasswordDto {
@IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(
/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
{ message: '密码必须包含字母和数字且长度在6~32之间' },
)
password: string;
}

View File

@@ -0,0 +1,35 @@
import {
IsEmail,
IsOptional,
IsString,
Length,
Matches,
} from 'class-validator';
export class UpdateDto {
@IsString({ message: '用户名不得为空' })
@Length(4, 32, { message: '用户名长度只能为4~32' })
username: string;
@IsString({ message: '昵称不得为空' })
@Length(1, 30, { message: '昵称长度只能为1~30' })
nickname: string;
@IsOptional()
@IsEmail({}, { message: '请输入有效的邮箱地址', always: false })
@Length(6, 254, {
message: '邮箱长度只能为6~254',
// 仅在值不为 null 或 undefined 时验证
always: false,
})
email?: string;
@IsOptional() // 标记字段为可选
@IsString({ message: '手机号不得为空', always: false })
@Matches(/^1[3456789]\d{9}$/, {
message: '请输入有效的手机号码',
// 仅在值不为 null 或 undefined 时验证
always: false,
})
phone?: string;
}

View File

@@ -0,0 +1,22 @@
import { IsEnum, IsString } from 'class-validator';
import { BlogPermission } from 'src/blog/blog.permission.enum';
export class CreateBlogDto {
@IsString()
title: string;
@IsString()
slug: string;// 允许空串但如果为空则需要手动设置为null防止数据库唯一键冲突
@IsString()
description: string;
@IsString()
contentUrl: string;
@IsEnum(BlogPermission, { each: true, message: '请求类型错误' })
permissions: BlogPermission[];
@IsString()
password: string; // 允许空串
}

View File

@@ -0,0 +1,28 @@
import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
class ResourceTagDto {
@IsString()
name: string;
@IsString()
type: string;
}
export class CreateResourceDto {
@IsString()
title: string;
@IsString()
description: string;
@IsString()
imageUrl: string;
@IsString()
link: string;
@ValidateNested({ each: true })
@Type(() => ResourceTagDto)
tags: ResourceTagDto[];
}

View File

@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class SetBlogPasswordDto {
@IsString()
password: string;
}

View File

@@ -0,0 +1,19 @@
import { IsEnum, IsString } from 'class-validator';
import { BlogPermission } from 'src/blog/blog.permission.enum';
export class UpdateBlogDto {
@IsString()
title: string;
@IsString()
description: string;
@IsString()
slug: string;
@IsString()
contentUrl: string;
@IsEnum(BlogPermission, { each: true, message: '请求类型错误' })
permissions: BlogPermission[];
}

View File

@@ -0,0 +1,16 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, Min } from 'class-validator';
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
pageSize?: number = 20;
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Resource } from "src/resource/entity/resource.entity";
import { Repository } from "typeorm";
@Injectable()
export class AdminResourceService {
constructor(
@InjectRepository(Resource)
private readonly resourceRepository: Repository<Resource>,
) { }
async findAll() {
return this.resourceRepository.find({
order: {
updatedAt: 'DESC',
}
});
}
async findById(id: string): Promise<Resource> {
return this.resourceRepository.findOne({ where: { id } });
}
async create(data: Partial<Resource>): Promise<Resource> {
const resource = this.resourceRepository.create(data);
return this.resourceRepository.save(resource);
}
async update(data: Partial<Resource>): Promise<Resource> {
// const updateRes = await this.resourceRepository.update(id, data);
// updateRes.affected
// return this.resourceRepository.findOne({ where: { id } });
return this.resourceRepository.save(data);
}
async delete(id: string): Promise<void> {
await this.resourceRepository.delete(id);
}
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -0,0 +1,61 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { VerificationModule } from './verification/verification.module';
import { NotificationModule } from './notification/notification.module';
import { ResourceModule } from './resource/resource.module';
import { BlogModule } from './blog/blog.module';
import { AdminModule } from './admin/admin.module';
import { OssModule } from './oss/oss.module';
import { ThrottlerModule } from '@nestjs/throttler';
import { CaptchaModule } from './captcha/captcha.module';
import { SmsModule } from './sms/sms.module';
import { CommonModule } from './common/common.module';
import { AppDataSource } from './data-source';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
useFactory: () => AppDataSource.options,
}),
ThrottlerModule.forRoot({
ignoreUserAgents: [/googlebot/i, /bingbot/i],
throttlers: [
{
name: 'min',
limit: 100,
ttl: 60 * 1000,
},
{
name: 'hour',
limit: 500,
ttl: 60 * 60 * 1000,
},
{
name: 'day',
limit: 10000,
ttl: 24 * 60 * 60 * 1000,
},
],
}),
UserModule,
AuthModule,
VerificationModule,
NotificationModule,
ResourceModule,
BlogModule,
AdminModule,
OssModule,
CaptchaModule,
SmsModule,
CommonModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,183 @@
import {
BadRequestException,
Body,
Controller,
Post,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { LoginByPasswordDto } from './dto/login.dto';
import { AuthService } from './auth.service';
import { UserSessionService } from 'src/auth/service/user-session.service';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import { Request, Response } from 'express';
import { UserService } from 'src/user/user.service';
import { AuthGuard } from './guards/auth.guard';
import { SmsLoginDto } from './dto/sms-login.dto';
import { SmsService } from 'src/sms/sms.service';
import { UserSession } from 'src/auth/entity/user-session.entity';
import { PasskeyService } from './service/passkey.service';
import { v4 as uuidv4 } from 'uuid';
import { PasskeyLoginDto } from './dto/passkey-login.dto';
import { AuthUser, CurrentUser } from './decorator/current-user.decorator';
import { PasskeyRegisterDto } from './dto/passkey-register.dto';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
private readonly userSessionService: UserSessionService,
private readonly smsService: SmsService,
private readonly passkeyService: PasskeyService,
) { }
private setUserSession(res: Response, session: UserSession) {
res.cookie('session', session.sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
// 永不过期不用设置maxAge
path: '/',
})
}
@Post('login/password')
@UseGuards(ThrottlerGuard)
@Throttle({
'min': { limit: 5, ttl: 60 * 1000 },
'hour': { limit: 20, ttl: 60 * 60 * 1000 },
'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 }
})
async loginByPassword(
@Body() loginDto: LoginByPasswordDto,
@Res({ passthrough: true }) res: Response,
) {
const { identifier, password } = loginDto;
const session = await this.authService.loginWithPassword(identifier, password);
this.setUserSession(res, session);
return {
user: await this.userService.findById(session.userId),
};
}
@Post('login/sms')
@UseGuards(ThrottlerGuard)
@Throttle({
'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 }
})
async loginBySms(
@Body() dto: SmsLoginDto,
@Res({ passthrough: true }) res: Response,
) {
const { phone, code } = dto;
await this.smsService.checkSms(phone, 'login', code);
// 验证通过,(注册并)登陆
const session = await this.authService.loginWithPhone(phone);
this.setUserSession(res, session);
return {
user: await this.userService.findById(session.userId),
}
}
@Post('passkey/login/options')
@UseGuards(ThrottlerGuard)
@Throttle({
'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 }
})
async loginByPasskeyOptions(
@Res({ passthrough: true }) res: Response,
) {
const tempSessionId = uuidv4();
const options = await this.passkeyService.getAuthenticationOptions(tempSessionId);
res.cookie('passkey_temp_session', tempSessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/api/auth/passkey/login',
maxAge: 1 * 60 * 1000,
});
return options;
}
@Post('passkey/login')
@UseGuards(ThrottlerGuard)
@Throttle({
'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 }
})
async loginByPasskey(
@Req() req: Request,
@Body() body: PasskeyLoginDto,
@Res({ passthrough: true }) res: Response,
) {
const tempSessionId = req.cookies?.passkey_temp_session;
if (!tempSessionId) {
throw new BadRequestException('登录失败,请重试');
}
try {
const user = await this.passkeyService.login(tempSessionId, body.credentialResponse);
const session = await this.userSessionService.createSession(user.userId);
this.setUserSession(res, session);
return {
user: await this.userService.findById(user.userId),
};
} catch (error) {
throw error;
} finally {
res.clearCookie('passkey_temp_session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/api/auth/passkey/login',
});
}
}
@UseGuards(AuthGuard)
@Post('passkey/register/options')
async getPasskeyRegisterOptions(
@CurrentUser() user: AuthUser,
) {
const { userId } = user;
return this.passkeyService.getRegistrationOptions(userId);
}
@UseGuards(AuthGuard)
@Post('passkey/register')
async registerPasskey(
@CurrentUser() user: AuthUser,
@Body() dto: PasskeyRegisterDto,
) {
const { userId } = user;
const { credentialResponse, name } = dto;
const passkey = await this.passkeyService.register(userId, credentialResponse, name.trim());
return {
id: passkey.id,
name: passkey.name,
createdAt: passkey.createdAt,
};
}
@UseGuards(AuthGuard)
@Post('logout')
async logout(@CurrentUser() user: AuthUser, @Res({ passthrough: true }) res: Response) {
const { sessionId } = user;
await this.userSessionService.invalidateSession(sessionId, '用户主动登出');
res.clearCookie('session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
return true;
}
}

View File

@@ -0,0 +1,28 @@
import { forwardRef, Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSession } from 'src/auth/entity/user-session.entity';
import { ConfigModule } from '@nestjs/config';
import { VerificationModule } from 'src/verification/verification.module';
import { AuthGuard } from './guards/auth.guard';
import { OptionalAuthGuard } from './guards/optional-auth.guard';
import { SmsModule } from 'src/sms/sms.module';
import { PasskeyCredential } from './entity/passkey-credential.entity';
import { UserSessionService } from './service/user-session.service';
import { PasskeyService } from './service/passkey.service';
@Module({
imports: [
ConfigModule,
forwardRef(() => UserModule),
TypeOrmModule.forFeature([UserSession, PasskeyCredential]),
VerificationModule,
SmsModule,
],
controllers: [AuthController],
providers: [AuthService, UserSessionService, PasskeyService, AuthGuard, OptionalAuthGuard],
exports: [AuthService, UserSessionService, PasskeyService, AuthGuard, OptionalAuthGuard],
})
export class AuthModule { }

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,76 @@
import { createHash } from 'crypto';
import { BadRequestException, Injectable } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
import { UserSessionService } from 'src/auth/service/user-session.service';
import { BusinessException } from 'src/common/exceptions/business.exception';
import { ErrorCode } from 'src/common/constants/error-codes';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly userSessionService: UserSessionService,
) { }
async loginWithPassword(identifier: string, password: string) {
// 依次使用邮箱、手机号、账号登陆(防止有大聪明给账号改成别人的邮箱或手机号)
const user = await this.userService.findOne(
[{ email: identifier }, { phone: identifier }, { username: identifier }],
{
withDeleted: true,
},
);
if (user && user.deletedAt !== null) {
throw new BusinessException({
message: '该账号注销中',
code: ErrorCode.USER_ACCOUNT_DEACTIVATED,
});
}
if (user === null || !user.password_hash || !user.salt) {
throw new BusinessException({
message: '账户或密码错误',
code: ErrorCode.AUTH_INVALID_CREDENTIALS
});
}
// 判断密码是否正确
const hashedPassword = this.hashPassword(password, user.salt);
if (hashedPassword !== user.password_hash) {
throw new BusinessException({
message: '账户或密码错误',
code: ErrorCode.AUTH_INVALID_CREDENTIALS
});
}
const { userId } = user;
return this.userSessionService.createSession(userId);
}
async loginWithPhone(phone: string) {
// 判断用户是否存在,若不存在则进行注册
let user = await this.userService.findOne({ phone }, { withDeleted: true });
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中,请使用其他手机号');
}
if (!user) {
// 执行注册操作
user = await this.userService.register({ phone });
}
if (!user || !user.userId) {
// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
return this.userSessionService.createSession(user.userId);
}
private hashPassword(password: string, salt: string): string {
return createHash('sha256').update(`${password}${salt}`).digest('hex');
}
}

View File

@@ -0,0 +1,14 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
export interface AuthUser {
sessionId: string;
userId: string;
}
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): AuthUser => {
const request = ctx.switchToHttp().getRequest<Request>();
return request.user;
},
);

View File

@@ -0,0 +1,37 @@
import { IsEnum, IsString, Length, ValidateIf } from 'class-validator';
// export class LoginDto {
// @IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' })
// type: 'password' | 'phone' | 'email';
// @ValidateIf((o) => o.type === 'password')
// account?: string;
// @ValidateIf((o) => o.type === 'phone')
// @IsString({ message: '手机号必须输入' })
// @Length(11, 11, { message: '手机号异常' }) // 中国大陆11位数字
// phone?: string;
// @ValidateIf((o) => o.type === 'email')
// @IsString({ message: '邮箱必须输入' })
// @Length(6, 254, { message: '邮箱异常' }) // RFC 5321
// email?: string;
// @ValidateIf((o) => o.type === 'phone' || o.type === 'email')
// @IsString({ message: '验证码必须输入' })
// @Length(6, 6, { message: '验证码异常' }) // 6位数字
// code?: string;
// }
export class LoginByPasswordDto {
@IsString({ message: '账户必须输入' })
@Length(1, 254, { message: '账户异常' }) // 用户名、邮箱、手机号
identifier: string;
@IsString({ message: '密码必须输入' })
@Length(6, 32, { message: '密码异常' }) // 6-32位
password: string;
}

View File

@@ -0,0 +1,6 @@
import { IsObject } from "class-validator";
export class PasskeyLoginDto {
@IsObject()
credentialResponse: any;
}

View File

@@ -0,0 +1,9 @@
import { IsObject, IsString } from "class-validator";
export class PasskeyRegisterDto {
@IsObject()
credentialResponse: any;
@IsString({ message: '通行证名称只能是字符串' })
name: string;
}

View File

@@ -0,0 +1,13 @@
import { IsPhoneNumber, Matches } from "class-validator";
export class SmsLoginDto {
@IsPhoneNumber('CN', {
message: '请输入有效的中国大陆手机号',
})
phone: string;
@Matches(/^\d{6}$/, {
message: '验证码必须是6位数字',
})
code: string;
}

View File

@@ -0,0 +1,36 @@
import { User } from "src/user/entities/user.entity";
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity()
@Index(['user'])
export class PasskeyCredential {
@PrimaryGeneratedColumn('uuid')
id: string;
// 关联用户
@ManyToOne(() => User, user => user.passkeys, { onDelete: 'CASCADE' })
user: User;
// WebAuthn 必需字段
@Column({ length: 255 })
name: string; // 用户自定义名称,如 "iPhone", "工作笔记本"
@Column({ unique: true })
credentialId: string; // Base64URL 编码的 credentialId唯一标识
@Column({ type: 'text' })
publicKey: string; // Base64URL 编码的公钥SPKI 格式)
@Column({ type: 'int' })
signCount: number; // 防重放攻击,每次签名递增
// 是否已验证(注册时验证,登录时更新)
@Column({ default: false })
verified: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,25 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class UserSession {
@PrimaryGeneratedColumn('uuid')
sessionId: string;
@Column({ length: 36 })
userId: string;
@Column({ nullable: true })
disabledReason?: string;
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@DeleteDateColumn({ nullable: true, precision: 3 })
deletedAt: Date;
}

View File

@@ -0,0 +1,34 @@
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { UserSessionService } from 'src/auth/service/user-session.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private userSessionService: UserSessionService,
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
// 从 Cookie 读取 session
const sessionId = request.cookies?.['session'];
if (!sessionId) {
throw new UnauthorizedException('登陆凭证无效,请重新登陆');
}
// 验证 session
const session = await this.userSessionService.getSession(sessionId);
if (!session) {
throw new UnauthorizedException('登陆凭证无效,请重新登陆');
}
const { userId } = session;
request.user = {
sessionId,
userId,
};
return true;
}
}

View File

@@ -0,0 +1,16 @@
import { ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard } from "./auth.guard";
@Injectable()
export class OptionalAuthGuard extends AuthGuard {
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
return await super.canActivate(context);
} catch (error) {
// 验证失败时req.user = null但允许继续
const request = context.switchToHttp().getRequest();
request.user = null;
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
export enum Role {
Admin = 'admin',
}

View File

@@ -0,0 +1,249 @@
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { PasskeyCredential } from "../entity/passkey-credential.entity";
import { Repository } from "typeorm";
import { User } from "src/user/entities/user.entity";
import { randomBytes } from 'crypto';
import { generateAuthenticationOptions, GenerateAuthenticationOptionsOpts, generateRegistrationOptions, GenerateRegistrationOptionsOpts, VerifiedAuthenticationResponse, VerifiedRegistrationResponse, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
import { isoBase64URL } from '@simplewebauthn/server/helpers';
interface ChallengeEntry {
value: string;
expiresAt: number;
}
class MemoryChallengeStore {
private store = new Map<string, ChallengeEntry>();
private cleanupInterval: NodeJS.Timeout | null = null;
constructor(private ttlMs: number = 5 * 60 * 100) {
this.startCleanup();
}
set(key: string, value: string): void {
this.store.set(key, {
value,
expiresAt: Date.now() + this.ttlMs,
});
}
get(key: string): string | null {
const entry = this.store.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}
return entry.value;
}
delete(key: string): void {
this.store.delete(key);
}
private startCleanup(): void {
this.cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.store.entries()) {
if (now > entry.expiresAt) {
this.store.delete(key);
}
}
}, 60_000); // 每分钟清理一次
}
stopCleanup(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
}
const registrationChallenges = new MemoryChallengeStore(5 * 60 * 1000); // 5 分钟过期
const authenticationChallenges = new MemoryChallengeStore(5 * 60 * 1000);
@Injectable()
export class PasskeyService implements OnModuleDestroy {
private readonly rpID: string;
private readonly origin: string;
private readonly rpName: string;
constructor(
@InjectRepository(PasskeyCredential)
private readonly passkeyRepo: Repository<PasskeyCredential>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
this.rpID = process.env.WEBAUTHN_RP_ID;
this.origin = process.env.WEBAUTHN_ORIGIN;
this.rpName = process.env.WEBAUTHN_RP_NAME;
if (!this.rpID || !this.origin || !this.rpName) {
throw new Error('Missing required env: WEBAUTHN_RP_ID or WEBAUTHN_ORIGIN');
}
}
onModuleDestroy() {
registrationChallenges.stopCleanup();
authenticationChallenges.stopCleanup();
}
private generateChallenge(length: number = 32): string {
return randomBytes(length).toString('base64');
}
async getRegistrationOptions(userId: string) {
const user = await this.userRepository.findOneBy({ userId });
if (!user) {
throw new NotFoundException('用户不存在');
}
const challenge = this.generateChallenge();
const opts: GenerateRegistrationOptionsOpts = {
rpName: this.rpName,
rpID: this.rpID,
userID: Buffer.from(userId),
userName: user.username || 'user',
userDisplayName: user.nickname || 'User',
challenge,
authenticatorSelection: {
residentKey: 'required', // 必须是可发现凭证Passkey
userVerification: 'preferred',
},
supportedAlgorithmIDs: [-7], // ES256
timeout: 60000,
};
const options = await generateRegistrationOptions(opts);
registrationChallenges.set(userId, options.challenge)
return options;
}
async register(userId: string, credentialResponse: any, name: string): Promise<PasskeyCredential> {
const expectedChallenge = registrationChallenges.get(userId);
if (!expectedChallenge) {
throw new BadRequestException('注册失败,请重试');
}
let verification: VerifiedRegistrationResponse;
try {
verification = await verifyRegistrationResponse({
response: credentialResponse,
expectedChallenge,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
requireUserVerification: false,
});
} catch (err) {
throw new BadRequestException('注册失败');
}
if (!verification.verified) {
throw new BadRequestException('注册失败');
}
const { credential } = verification.registrationInfo;
if (!credential) {
throw new InternalServerErrorException('服务器内部错误');
}
// 保存凭证到数据库
const passkey = this.passkeyRepo.create({
user: { userId } as User,
name: name || '新的通行证',
credentialId: credential.id,
publicKey: isoBase64URL.fromBuffer(credential.publicKey),
signCount: credential.counter,
verified: true,
});
await this.passkeyRepo.save(passkey);
registrationChallenges.delete(userId);
return passkey;
}
async getAuthenticationOptions(sessionId: string) {
const challenge = this.generateChallenge();
const opts: GenerateAuthenticationOptionsOpts = {
rpID: this.rpID,
challenge,
timeout: 60000,
userVerification: 'preferred',
};
const options = await generateAuthenticationOptions(opts);
authenticationChallenges.set(sessionId, options.challenge);
return options;
}
async login(sessionId: string, credentialResponse: any): Promise<User> {
const expectedChallenge = authenticationChallenges.get(sessionId);
if (!expectedChallenge) {
throw new BadRequestException('认证失败,请重试');
}
const credentialId = credentialResponse.id;
const passkey = await this.passkeyRepo.findOne({
where: { credentialId, verified: true },
relations: ['user'],
});
if (!passkey) {
throw new NotFoundException('未找到可用的通行证');
}
let verification: VerifiedAuthenticationResponse;
try {
verification = await verifyAuthenticationResponse({
response: credentialResponse,
expectedChallenge,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
credential: {
id: passkey.credentialId,
publicKey: isoBase64URL.toBuffer(passkey.publicKey),
counter: passkey.signCount,
},
requireUserVerification: false,
});
} catch (err) {
throw new BadRequestException('认证失败');
}
if (!verification.verified) {
throw new BadRequestException('认证失败');
}
const newSignCount = verification.authenticationInfo.newCounter;
if (newSignCount !== passkey.signCount) {
passkey.signCount = newSignCount;
await this.passkeyRepo.save(passkey);
}
authenticationChallenges.delete(sessionId);
return passkey.user;
}
async listUserPasskeys(userId: string): Promise<PasskeyCredential[]> {
return this.passkeyRepo.find({
where: { user: { userId }, verified: true },
select: ['id', 'name', 'createdAt'],
});
}
async removePasskey(userId: string, passkeyId: string): Promise<void> {
const result = await this.passkeyRepo.delete({
id: passkeyId,
user: { userId },
});
if (result.affected === 0) {
throw new NotFoundException('未找到对应的通行证');
}
}
}

View File

@@ -0,0 +1,39 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { UserSession } from '../entity/user-session.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserSessionService {
constructor(
@InjectRepository(UserSession)
private readonly userSessionRepository: Repository<UserSession>,
) { }
async createSession(userId: string): Promise<UserSession> {
const session = this.userSessionRepository.create({
userId,
});
return this.userSessionRepository.save(session);
}
async getSession(sessionId: string) {
const session = await this.userSessionRepository.findOne({
where: {
sessionId,
},
});
return session;
}
async invalidateSession(sessionId: string, reason?: string): Promise<void> {
await this.userSessionRepository.update(
{ sessionId, deletedAt: null },
{
deletedAt: new Date(),
disabledReason: reason || null,
}
)
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BlogController } from './blog.controller';
describe('BlogController', () => {
let controller: BlogController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BlogController],
}).compile();
controller = module.get<BlogController>(BlogController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,145 @@
import {
BadRequestException,
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { BlogService } from './blog.service';
import { UserService } from 'src/user/user.service';
import { createBlogCommentDto } from './dto/create.blogcomment.dto';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import { BlogPermission } from './blog.permission.enum';
import { OptionalAuthGuard } from 'src/auth/guards/optional-auth.guard';
import { AuthUser, CurrentUser } from 'src/auth/decorator/current-user.decorator';
import { Request } from 'express';
@Controller('blog')
export class BlogController {
constructor(
private readonly blogService: BlogService,
private readonly userService: UserService,
) { }
@Get()
getBlogs() {
return this.blogService.list();
}
@Get(':id/slug')
async getBlogBySlug(
@Param('id') slug: string,
@Query('p') password?: string,
) {
if (slug.trim().length === 0) {
throw new BadRequestException('文章不存在');
}
const blog = await this.blogService.findBySlug(slug);
if (!blog) throw new BadRequestException('文章不存在或无权限访问');
if (!blog.permissions.includes(BlogPermission.Public)) {
// 无公开权限,则进一步检查是否有密码保护
if (!blog.permissions.includes(BlogPermission.ByPassword)) {
throw new BadRequestException('文章不存在或无权限访问');
} else {
// 判断密码是否正确
if (
typeof password !== 'string' ||
this.blogService.hashPassword(password) !== blog.password_hash
) {
throw new BadRequestException('文章不存在或无权限访问');
}
}
}
const blogDataRes = await fetch(`${blog.contentUrl}`);
const blogContent = await blogDataRes.text();
this.blogService.incrementViewCount(blog.id).catch(() => null);
return {
id: blog.id,
title: blog.title,
description: blog.description,
createdAt: blog.createdAt,
content: blogContent,
};
}
@Get(':id/comments')
async getBlogComments(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
/** @todo 对文章可读性进行更详细的判定 */
if (
!blog.permissions.includes(BlogPermission.Public) &&
!blog.permissions.includes(BlogPermission.ByPassword)
) {
throw new BadRequestException('文章不存在或未公开');
}
return await this.blogService.getComments(blog);
}
// 该接口允许匿名评论但仍需验证userId合法性
@UseGuards(ThrottlerGuard, OptionalAuthGuard)
@Throttle({ default: { limit: 20, ttl: 60000 } })
@Post(':id/comment')
async createBlogComment(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() commentData: createBlogCommentDto,
@Req() req: Request,
@CurrentUser() authUser: AuthUser,
) {
const { userId } = (authUser ?? {}) as { userId: string | undefined };
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
if (!blog.permissions.includes(BlogPermission.AllowComments)) {
throw new BadRequestException('作者关闭了该文章的评论功能');
}
const user = userId ? await this.userService.findOne({ userId }) : null;
const ip = `${req.headers['x-forwarded-for'] || req.ip}`;
// 获取IP归属地
let address = '未知';
if (!['::1'].includes(ip)) {
const addressRes = await (
await fetch(
`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=${ip}`,
)
).json();
if (addressRes?.code == 0) {
const country: string = addressRes?.data?.countryCN || '未知';
const province: string = addressRes?.data?.provinceCN || '中国';
if (country !== '中国') {
// 非中国,显示国家
address = country;
} else {
// 中国,显示省份
address = province;
}
}
}
const comment = {
...commentData,
blog,
user,
ip,
address,
};
return await this.blogService.createComment(comment);
}
}

View File

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { BlogController } from './blog.controller';
import { BlogService } from './blog.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Blog } from './entity/Blog.entity';
import { BlogComment } from './entity/BlogComment.entity';
import { AuthModule } from 'src/auth/auth.module';
import { UserModule } from 'src/user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([Blog, BlogComment]),
AuthModule,
UserModule,
],
controllers: [BlogController],
providers: [BlogService],
exports: [BlogService],
})
export class BlogModule {}

View File

@@ -0,0 +1,6 @@
export enum BlogPermission {
Public = 'Public',
ByPassword = 'ByPassword',
List = 'List',
AllowComments = 'AllowComments',
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BlogService } from './blog.service';
describe('BlogService', () => {
let service: BlogService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BlogService],
}).compile();
service = module.get<BlogService>(BlogService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,154 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Blog } from './entity/Blog.entity';
import { Repository } from 'typeorm';
import { BlogComment } from './entity/BlogComment.entity';
import { BlogPermission } from './blog.permission.enum';
import { createHash } from 'crypto';
@Injectable()
export class BlogService {
constructor(
@InjectRepository(Blog)
private readonly blogRepository: Repository<Blog>,
@InjectRepository(BlogComment)
private readonly blogCommentRepository: Repository<BlogComment>,
) { }
async list(
option: {
withAll?: boolean;
} = {},
) {
return (
await this.blogRepository.find({
order: {
createdAt: 'DESC',
},
})
)
.filter(
(i) => option.withAll || i.permissions.includes(BlogPermission.List),
)
.map((i) => {
if (option.withAll) {
return i;
}
const { createdAt, updatedAt, id, title, viewCount, description, slug } = i;
return {
createdAt,
updatedAt,
id,
title,
slug,
viewCount,
description,
};
});
}
async create(dto: Partial<Blog> & { password: string }) {
const { password, ...blog } = dto;
if (blog.permissions.includes(BlogPermission.ByPassword)) {
if (password) {
blog.password_hash = createHash('sha256')
.update(`${password}`)
.digest('hex');
}
}
if (typeof blog.slug === 'string' && blog.slug.trim().length === 0) {
blog.slug = null;
}
const newBlog = this.blogRepository.create(blog);
return this.blogRepository.save(newBlog);
}
async setPassword(id: string, password: string) {
const blog = await this.findById(id);
if (!blog) {
throw new Error('博客不存在');
}
return (
(
await this.blogRepository.update(id, {
...blog,
password_hash: this.hashPassword(password),
})
).affected > 0
);
}
async update(id: string, blog: Partial<Blog>) {
await this.blogRepository.update(id, blog);
return this.blogRepository.findOneBy({ id });
}
async remove(id: string) {
const blog = await this.blogRepository.findOneBy({ id });
if (!blog) return null;
return this.blogRepository.softRemove(blog);
}
async findById(id: string) {
return await this.blogRepository.findOneBy({ id });
}
async findBySlug(slug: string) {
return this.blogRepository.findOne({
where: { slug }
})
}
async incrementViewCount(id: string) {
await this.blogRepository.increment({ id }, 'viewCount', 1);
}
async getComments(blog: Blog) {
const comments = await this.blogCommentRepository.find({
where: { blog: { id: blog.id } },
relations: ['user'],
order: {
createdAt: 'DESC',
},
});
return comments.map((comment) => {
const { user, ...rest } = comment;
delete rest.blog;
return {
...rest,
user: user
? {
userId: user.userId,
username: user.username,
nickname: user.nickname,
}
: null,
};
});
}
async createComment(comment: Partial<BlogComment>) {
const newComment = this.blogCommentRepository.create(comment);
const savedComment = await this.blogCommentRepository.save(newComment, {});
const { user, ...commentWithoutBlog } = savedComment;
delete commentWithoutBlog.blog;
return {
...commentWithoutBlog,
user: user
? {
userId: user.userId,
username: user.username,
nickname: user.nickname,
}
: null,
};
}
hashPassword(password: string) {
return createHash('sha256').update(`${password}`).digest('hex');
}
}

View File

@@ -0,0 +1,10 @@
import { IsOptional, IsString, IsUUID } from 'class-validator';
export class createBlogCommentDto {
@IsString({ message: '评论内容不能为空' })
content: string;
@IsOptional()
@IsUUID('4', { message: '父评论ID格式错误' })
parentId?: string;
}

View File

@@ -0,0 +1,53 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BlogComment } from './BlogComment.entity';
import { BlogPermission } from '../blog.permission.enum';
/** @todo 考虑后续将权限的数据类型替换为json以提高查询效率 */
@Entity()
export class Blog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, nullable: true })
slug: string;
@Column()
title: string;
@Column()
description: string;
@Column()
contentUrl: string;
@Column({ default: 0 })
viewCount: number;
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@UpdateDateColumn({ precision: 3 })
updatedAt: Date;
@DeleteDateColumn({ precision: 3, nullable: true })
deletedAt: Date;
// 权限
@Column('simple-array', { default: '' })
permissions: BlogPermission[];
@Column({ nullable: true })
password_hash: string | null;
// 关系
@OneToMany(() => BlogComment, (comment) => comment.blog)
comments: BlogComment[];
}

View File

@@ -0,0 +1,43 @@
import { User } from 'src/user/entities/user.entity';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Blog } from './Blog.entity';
@Entity()
export class BlogComment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
content: string;
@Column()
ip: string;
@Column()
address: string;
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@DeleteDateColumn({ precision: 3, nullable: true })
deletedAt: Date;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'userId' })
user: User | null;
@ManyToOne(() => Blog)
@JoinColumn({ name: 'blogId' })
blog: Blog | null;
@Column({ type: 'uuid', nullable: true })
parentId: string | null;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CaptchaController } from './captcha.controller';
describe('CaptchaController', () => {
let controller: CaptchaController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CaptchaController],
}).compile();
controller = module.get<CaptchaController>(CaptchaController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,10 @@
import { Controller, Get } from '@nestjs/common';
import { GetCaptchaDto } from './dto/get-captcha.dto';
@Controller('captcha')
export class CaptchaController {
@Get()
async getCaptcha(dto: GetCaptchaDto) {
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { CaptchaService } from './captcha.service';
import { CaptchaController } from './captcha.controller';
import { CaptchaRateLimitService } from './service/rate-limit';
@Module({
providers: [CaptchaService, CaptchaRateLimitService],
controllers: [CaptchaController],
imports: [],
})
export class CaptchaModule { }

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CaptchaService } from './captcha.service';
describe('CaptchaService', () => {
let service: CaptchaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CaptchaService],
}).compile();
service = module.get<CaptchaService>(CaptchaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { ErrorCode } from 'src/common/constants/error-codes';
import { BusinessException } from 'src/common/exceptions/business.exception';
export enum CaptchaContext {
SEND_SMS = 'send_sms',
PASSKEY = 'passkey',
}
@Injectable()
export class CaptchaService {
public async generate(context: CaptchaContext, ip: string, userId?: string) {
await this.checkRateLimit(ip, context)
}
public async verify(token: string, ip: string, userId?: string) {
}
private async checkRateLimit(ip: string, context: CaptchaContext) {
/** @todo */
throw new BusinessException({
code: ErrorCode.CAPTCHA_RARE_LIMIT,
message: '服务器处理不过来了,过会儿再试试吧',
});
}
}

View File

@@ -0,0 +1,16 @@
import { IsEnum, IsOptional, IsUUID } from "class-validator";
export enum CaptchaContext {
SEND_SMS = 'send_sms',
PASSKEY = 'passkey',
}
export class GetCaptchaDto {
@IsEnum(CaptchaContext, { message: '无效的context' })
context: string;
@IsOptional()
@IsUUID('4', { message: 'userId不合法' })
userId?: string;
}

View File

@@ -0,0 +1,3 @@
export class CaptchaRateLimitService {
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { RolesGuard } from './guard/roles.guard';
import { UserModule } from 'src/user/user.module';
@Module({
providers: [RolesGuard],
imports: [UserModule],
exports: [RolesGuard],
})
export class CommonModule { }

View File

@@ -0,0 +1,47 @@
/**
* 全局业务错误码规范:
* - 每个模块分配一个 1000 起始的段(如 USER: -1000~1999, AUTH: -2000~2999
* - 代码结构:{ 模块名大写 }_{ 错误语义 }
*/
export const ErrorCode = {
// 通用错误0 ~ 999
COMMON_INTERNAL_ERROR: -1,
COMMON_INVALID_PARAM: -2,
COMMON_NOT_FOUND: -3,
// 用户模块1000 ~ 1999
USER_NOT_FOUND: -1001,
USER_ALREADY_EXISTS: -1002,
USER_ACCOUNT_DISABLED: -1003,
USER_FIND_OPTIONS_EMPTY: -1004,
USER_ACCOUNT_DEACTIVATED: -1005,
// 认证模块
AUTH_INVALID_CREDENTIALS: -2001,
AUTH_PASSKEY_NOT_REGISTERED: -2002,
AUTH_SESSION_EXPIRED: -2003,
// 博客模块
BLOG_NOT_FOUND: -3001,
BLOG_PERMISSION_DENIED: -3002,
// 验证模块
CAPTCHA_RARE_LIMIT: -4001,
// 通知模块
NOTIFICATION_SEND_FAILED: -5001,
// Sms模块
SMS_CODE_INCORRECT: -6001,
SMS_CODE_EXPIRED: -6002,
// 资源模块
RESOURCE_UPLOAD_FAILED: -7001,
RESOURCE_NOT_FOUND: -7002,
// 管理员模块
ADMIN_FORBIDDEN: -8001,
} as const;
export type ErrorCodeType = typeof ErrorCode[keyof typeof ErrorCode];

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from 'src/auth/role.enum';
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);

View File

@@ -0,0 +1,22 @@
import { HttpStatus } from '@nestjs/common';
export class BusinessException {
public statusCode: HttpStatus;
public message: string;
public code: number;
public data: any;
constructor(args: {
statusCode?: HttpStatus,
message?: string,
code?: number,
data?: any,
}) {
const { statusCode, message, code, data } = args;
this.statusCode = statusCode || HttpStatus.BAD_REQUEST;
this.message = message || '请求错误';
this.code = code || -1;
this.data = data || null;
}
}

View File

@@ -0,0 +1,56 @@
import { ArgumentsHost, ExceptionFilter, HttpException, HttpStatus, Logger } from "@nestjs/common";
import { Request, Response } from "express";
import { BusinessException } from "../exceptions/business.exception";
export class GlobalExceptionsFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let errorResponse = {
success: false,
message: '服务器内部错误',
code: -1,
data: null as any,
};
if (exception instanceof BusinessException) {
statusCode = exception.statusCode;
const { message, code, data } = exception;
errorResponse = {
...errorResponse,
message, code, data,
}
} else if (exception instanceof HttpException) {
// 当HttpException传入类型为string时响应data为nullmessage为传入的string
// 其他请况object/number响应为传入数据message为HttpException的错误码
statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse() as Record<string, any>;
if (exceptionResponse.message) {
errorResponse.message = exceptionResponse.message;
} else {
errorResponse.message = '请求失败';
errorResponse.data = exceptionResponse;
}
if (statusCode === HttpStatus.UNAUTHORIZED && request.cookies?.['session']) {
response.clearCookie('session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
}
if (statusCode === HttpStatus.TOO_MANY_REQUESTS) {
errorResponse.message = '请求过于频繁,请稍后再试';
}
} else {
Logger.warn(exception, request.path);
}
response.status(statusCode).json(errorResponse);
}
}

View File

@@ -0,0 +1,59 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
InternalServerErrorException,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { AuthUser } from 'src/auth/decorator/current-user.decorator';
import { Role } from 'src/auth/role.enum';
import { UserService } from 'src/user/user.service';
@Injectable()
export class RolesGuard implements CanActivate {
private logger = new Logger(RolesGuard.name);
constructor(
private reflector: Reflector,
private readonly userService: UserService,
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<Role[] | undefined>(
'roles',
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest<Request>();
const authUser = request.user as AuthUser;
if (!authUser) {
this.logger.warn(
`Path: ${request.path} has RolesGuard enabled, but it seems AuthGuard was forgotten.`
)
throw new InternalServerErrorException('服务器内部错误');
}
const { userId } = authUser;
const user = await this.userService.findOne({ userId })
if (!user) {
this.logger.warn(
`UserId: ${user.userId} has a valid login credential, but the user information does not exist.`
)
throw new UnauthorizedException('用户不存在');
}
if (!requiredRoles.some((role) => user.roles.includes(role))) {
throw new ForbiddenException('权限不足');
}
return true;
}
}

View File

@@ -0,0 +1,25 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
map((data) => ({
success: true,
code: 0,
message: '请求成功',
data,
})),
);
}
}

View File

@@ -0,0 +1,9 @@
import { AuthUser } from "src/auth/decorator/current-user.decorator";
declare global {
namespace Express {
interface Request {
user?: AuthUser;
}
}
}

View File

@@ -0,0 +1,20 @@
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
dotenv.config();
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT ?? 5432),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
synchronize: false,
logging: false,
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
});

32
apps/backend/src/main.ts Normal file
View File

@@ -0,0 +1,32 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { BadRequestException, ValidationPipe } from '@nestjs/common';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import { GlobalExceptionsFilter } from './common/filters/global.exceptions.filter';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
exceptionFactory: (errors) => {
const error = errors[0];
const firstConstraint = error.constraints
? Object.values(error.constraints)[0]
: '验证失败';
throw new BadRequestException(firstConstraint);
},
}),
);
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new GlobalExceptionsFilter());
await app.listen(process.env.PORT ?? 3001);
}
bootstrap();

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddSlugToBlog1766809565876 implements MigrationInterface {
name = 'AddSlugToBlog1766809565876'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "blog" ADD "slug" character varying`);
await queryRunner.query(`ALTER TABLE "blog" ADD CONSTRAINT "UQ_0dc7e58d73a1390874a663bd599" UNIQUE ("slug")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "blog" DROP CONSTRAINT "UQ_0dc7e58d73a1390874a663bd599"`);
await queryRunner.query(`ALTER TABLE "blog" DROP COLUMN "slug"`);
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { NotificationService } from './notification.service';
@Module({
providers: [NotificationService],
exports: [NotificationService],
})
export class NotificationModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationService } from './notification.service';
describe('NotificationService', () => {
let service: NotificationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NotificationService],
}).compile();
service = module.get<NotificationService>(NotificationService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,143 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import Dm20151123, * as $Dm20151123 from '@alicloud/dm20151123';
import * as $OpenApi from '@alicloud/openapi-client';
// import Client, * as $dm from '@alicloud/dm20151123';
import * as $Util from '@alicloud/tea-util';
import Credential, { Config } from '@alicloud/credentials';
@Injectable()
export class NotificationService {
// private dm: Dm20151123;
constructor() {
// const credentialsConfig = new Config({
// type: 'access_key',
// accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
// accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
// });
// const credential = new Credential(credentialsConfig);
// const config = new $OpenApi.Config({ credential });
// config.endpoint = 'dm.aliyuncs.com';
// this.dm = new Dm20151123(config);
}
private getMailHtmlBody(option: { type: 'login-verify'; code: string }) {
if (option.type === 'login-verify') {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>特恩的日志 - 登录验证码</title>
<style>
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0px auto; padding: 20px; }
.content { padding: 20px 0; }
.code-box {
background: #f8f9fa;
border: 1px dashed #ccc;
padding: 15px;
text-align: center;
margin: 20px 0;
font-size: 24px;
font-weight: bold;
letter-spacing: 5px;
color: #e74c3c;
}
.footer {
color: #777;
font-size: 12px;
border-top: 1px solid #eee;
padding-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>您好!您正在尝试登录【特恩的日志】控制台,验证码如下:</p>
<div class="code-box">
<span id="verificationCode">${option.code}</span>
</div>
<p>请注意:</p>
<ul>
<li>此验证码 <strong>10分钟内</strong> 有效</li>
<li>请勿向任何人透露此验证码</li>
<li>如非本人操作,请忽略本邮件</li>
</ul>
</div>
<div class="footer">
<p>© 2025 TONE个人 版权所有</p>
<a href="https://beian.miit.gov.cn/">网站备案号渝ICP备2023009516号-1</a>
</div>
</div>
</body>
</html>`;
} else {
throw new Error('未配置的模版');
}
}
async sendMail(option: {
type: 'login-verify';
targetMail: string;
code: string;
}) {
// const runtime = new $Util.RuntimeOptions({});
// const singleSendMailRequest = new $Dm20151123.SingleSendMailRequest({
// accountName: 'security@tonesc.cn',
// addressType: 1,
// replyToAddress: false,
// toAddress: `${option.targetMail}`,
// subject: '【特恩的日志】登陆验证码',
// htmlBody: this.getMailHtmlBody({
// type: 'login-verify',
// code: option.code,
// }),
// textBody: '',
// });
// try {
// await this.dm.singleSendMailWithOptions(singleSendMailRequest, runtime);
// } catch (error) {
// console.error(error);
// throw new BadRequestException('邮件发送失败');
// }
throw new Error('not implement')
}
/**
* @deprecated 短信签名暂未通过
*/
async sendSMS(phone: string, type: 'login', code: string) {
throw new Error(
`SMS sending is not implemented yet. Phone: ${phone}, Type: ${type}, Code: ${code}`,
);
// const config = new $OpenApi.Config({
// accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
// accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
// })
// config.endpoint = 'dysmsapi.aliyuncs.com';
// const client = new Client(config);
// const request = new $dysmsapi.SendSmsRequest({});
// request.phoneNumbers = phone;
// request.signName = (() => {
// switch (type) {
// case 'login':
// return process.env.ALIYUN_SMS_LOGIN_SIGN_NAME;
// default:
// throw new Error('Unknown SMS type');
// }
// })();
// request.templateCode = code;
// await client.sendSms(request).then(a => {
// console.log(a)
// }).catch(err => {
// console.error(err);
// })
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OssController } from './oss.controller';
describe('OssController', () => {
let controller: OssController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [OssController],
}).compile();
controller = module.get<OssController>(OssController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,19 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { OssService } from './oss.service';
import { AuthGuard } from 'src/auth/guards/auth.guard';
import { AuthUser, CurrentUser } from 'src/auth/decorator/current-user.decorator';
@Controller('oss')
export class OssController {
constructor(private readonly ossService: OssService) { }
@UseGuards(AuthGuard)
@Get('sts')
async getStsToken(@CurrentUser() user: AuthUser) {
const { userId } = user;
return {
...(await this.ossService.getStsToken(`${userId}`)),
userId,
};
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { OssService } from './oss.service';
import { OssController } from './oss.controller';
import { AuthModule } from 'src/auth/auth.module';
import { UserModule } from 'src/user/user.module';
@Module({
providers: [OssService],
controllers: [OssController],
imports: [AuthModule, UserModule],
})
export class OssModule { }

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OssService } from './oss.service';
describe('OssService', () => {
let service: OssService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [OssService],
}).compile();
service = module.get<OssService>(OssService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { STS } from 'ali-oss';
@Injectable()
export class OssService {
private sts = new STS({
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
});
private stsCache: {
[session: string]: {
credentials: {
AccessKeyId: string;
AccessKeySecret: string;
SecurityToken: string;
Expiration: string;
};
expireTime: number; // 时间戳,单位为毫秒
};
} = {};
/** @todo 该方法存在缓存穿透问题,待优化 */
async getStsToken(session: string) {
if (this.stsCache[session]) {
const cached = this.stsCache[session];
// 检查缓存是否过期
if (cached.expireTime > Date.now()) {
return cached.credentials;
} else {
// 如果过期,删除缓存
delete this.stsCache[session];
}
}
return this.sts
.assumeRole(process.env.ALIYUN_OSS_STS_ROLE_ARN, ``, 3600, `${session}`)
.then((res) => {
// 缓存
this.stsCache[session] = {
credentials: res.credentials,
expireTime:
new Date(res.credentials.Expiration).getTime() - 5 * 60 * 1000, // 提前5分钟过期,
};
return res.credentials;
})
.catch((err) => {
console.error('获取STS Token失败:', err);
throw new Error('获取STS Token失败');
});
}
}

View File

@@ -0,0 +1,50 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
type ResourceTag = {
name: string;
type: string;
};
@Entity()
export class Resource {
@PrimaryGeneratedColumn('uuid')
@Index()
id: string;
@Column()
title: string;
@Column()
description: string;
@Column()
imageUrl: string;
@Column()
link: string;
@Column('jsonb')
tags: ResourceTag[];
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@UpdateDateColumn({ precision: 3 })
updatedAt: Date;
}
export interface PublicResource {
id: string;
title: string;
description: string;
imageUrl: string;
link: string;
tags: ResourceTag[];
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ResourceController } from './resource.controller';
describe('ResourceController', () => {
let controller: ResourceController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ResourceController],
}).compile();
controller = module.get<ResourceController>(ResourceController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More