feat: 安全增强 + 删除密码重置申请功能 + 登录提醒开关

安全增强:
- 新增 SSRF、XXE、模板注入、敏感路径探测检测规则
- security/constants.py: 添加新的威胁类型和检测模式
- security/threat_detector.py: 实现新检测逻辑

删除密码重置申请功能:
- 移除 /api/password_resets 相关API
- 删除 password_reset_requests 数据库表
- 前端移除密码重置申请页面和菜单
- 用户只能通过邮��找回密码,未绑定邮箱需联系管理员

登录提醒全局开关:
- email_service.py: 添加 login_alert_enabled 字段
- routes/api_auth.py: 检查开关状态再发送登录提醒
- EmailPage.vue: 添加新设备登录提醒开关

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 12:08:36 +08:00
parent 4ba933b001
commit 89f3fd9759
65 changed files with 555 additions and 784 deletions

View File

@@ -1,34 +1,34 @@
{
"_email-DoKk83fr.js": {
"file": "assets/email-DoKk83fr.js",
"_email-BghJNgj1.js": {
"file": "assets/email-BghJNgj1.js",
"name": "email",
"imports": [
"index.html"
]
},
"_tasks-Bgkd54ac.js": {
"file": "assets/tasks-Bgkd54ac.js",
"_tasks-Cx_Yf55V.js": {
"file": "assets/tasks-Cx_Yf55V.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_update-BVJ0Pp6O.js": {
"file": "assets/update-BVJ0Pp6O.js",
"_update-D34iQbO6.js": {
"file": "assets/update-D34iQbO6.js",
"name": "update",
"imports": [
"index.html"
]
},
"_users-Bw5HW1mw.js": {
"file": "assets/users-Bw5HW1mw.js",
"_users-DCcrmSwH.js": {
"file": "assets/users-DCcrmSwH.js",
"name": "users",
"imports": [
"index.html"
]
},
"index.html": {
"file": "assets/index-CDhtYQo-.js",
"file": "assets/index-C9w-iZIr.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -44,11 +44,11 @@
"src/pages/SettingsPage.vue"
],
"css": [
"assets/index-DiIt7W4Z.css"
"assets/index-_5Ec1Hmd.css"
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-BSLa6sED.js",
"file": "assets/AnnouncementsPage-DEX_yASt.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
@@ -60,20 +60,20 @@
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-BHdschU6.js",
"file": "assets/EmailPage-Cev_X_Ce.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
"imports": [
"_email-DoKk83fr.js",
"_email-BghJNgj1.js",
"index.html"
],
"css": [
"assets/EmailPage-BxzHc6tN.css"
"assets/EmailPage-BH6ksrcc.css"
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-CLu2KtuG.js",
"file": "assets/FeedbacksPage-BKxylUkG.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
@@ -85,13 +85,13 @@
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-CbeqOQSe.js",
"file": "assets/LogsPage-CemQ-Y_T.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-Bw5HW1mw.js",
"_tasks-Bgkd54ac.js",
"_users-DCcrmSwH.js",
"_tasks-Cx_Yf55V.js",
"index.html"
],
"css": [
@@ -99,22 +99,22 @@
]
},
"src/pages/ReportPage.vue": {
"file": "assets/ReportPage-DVC8Kawd.js",
"file": "assets/ReportPage-D6vDD1zK.js",
"name": "ReportPage",
"src": "src/pages/ReportPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_email-DoKk83fr.js",
"_tasks-Bgkd54ac.js",
"_update-BVJ0Pp6O.js"
"_email-BghJNgj1.js",
"_tasks-Cx_Yf55V.js",
"_update-D34iQbO6.js"
],
"css": [
"assets/ReportPage-TpqQWWvU.css"
"assets/ReportPage-CSbGJlZV.css"
]
},
"src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-CuXCrXIZ.js",
"file": "assets/SecurityPage-DGvsGoGa.js",
"name": "SecurityPage",
"src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true,
@@ -126,7 +126,7 @@
]
},
"src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-cKC6DywW.js",
"file": "assets/SettingsPage-Bw1ItHlK.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
@@ -138,12 +138,12 @@
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-2XepJLfC.js",
"file": "assets/SystemPage-RgAQwtHu.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
"imports": [
"_update-BVJ0Pp6O.js",
"_update-D34iQbO6.js",
"index.html"
],
"css": [
@@ -151,16 +151,16 @@
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-CGNuB954.js",
"file": "assets/UsersPage-CFbr6Y3k.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-Bw5HW1mw.js",
"_users-DCcrmSwH.js",
"index.html"
],
"css": [
"assets/UsersPage-CbiPbpuj.css"
"assets/UsersPage-CC4Unpwt.css"
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-7a7e1e9d]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-7a7e1e9d]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-7a7e1e9d]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-head[data-v-7a7e1e9d]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-7a7e1e9d]{margin:0;font-size:14px;font-weight:800}.help[data-v-7a7e1e9d]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-7a7e1e9d]{overflow-x:auto}.stat-card[data-v-7a7e1e9d]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-7a7e1e9d]{font-size:20px;font-weight:900;line-height:1.1}.stat-label[data-v-7a7e1e9d]{margin-top:6px;font-size:12px;color:var(--app-muted)}.ok[data-v-7a7e1e9d]{color:#047857}.err[data-v-7a7e1e9d]{color:#b91c1c}.sub-stats[data-v-7a7e1e9d]{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.ellipsis[data-v-7a7e1e9d]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-7a7e1e9d]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-7a7e1e9d]{font-size:12px}.dialog-actions[data-v-7a7e1e9d]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-7a7e1e9d]{flex:1}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-ff849557]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-ff849557]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-ff849557]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-head[data-v-ff849557]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-ff849557]{margin:0;font-size:14px;font-weight:800}.help[data-v-ff849557]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-ff849557]{overflow-x:auto}.stat-card[data-v-ff849557]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-ff849557]{font-size:20px;font-weight:900;line-height:1.1}.stat-label[data-v-ff849557]{margin-top:6px;font-size:12px;color:var(--app-muted)}.ok[data-v-ff849557]{color:#047857}.err[data-v-ff849557]{color:#b91c1c}.sub-stats[data-v-ff849557]{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.ellipsis[data-v-ff849557]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-ff849557]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-ff849557]{font-size:12px}.dialog-actions[data-v-ff849557]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-ff849557]{flex:1}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{S as m,_ as T,r as p,e as u,f as h,g as k,h as r,j as a,w as s,p as x,L as i,K as b}from"./index-CDhtYQo-.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},A=T(N,[["__scopeId","data-v-2f4b840f"]]);export{A as default};
import{O as m,_ as T,r as p,d as u,e as h,f as k,g as r,h as a,w as s,n as x,J as d,I as b}from"./index-C9w-iZIr.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},I=T(N,[["__scopeId","data-v-2f4b840f"]]);export{I as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-d73d2b82]{display:flex;flex-direction:column;gap:12px}.card[data-v-d73d2b82]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-d73d2b82]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-d73d2b82]{margin-top:10px;font-size:12px}.table-wrap[data-v-d73d2b82]{overflow-x:auto}.user-block[data-v-d73d2b82]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-d73d2b82]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-d73d2b82]{font-size:12px}.vip-sub[data-v-d73d2b82]{font-size:12px;color:#7c3aed}.actions[data-v-d73d2b82]{display:flex;flex-wrap:wrap;gap:8px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-84b2f73a]{display:flex;flex-direction:column;gap:12px}.card[data-v-84b2f73a]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-84b2f73a]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-84b2f73a]{margin-top:10px;font-size:12px}.table-wrap[data-v-84b2f73a]{overflow-x:auto}.user-block[data-v-84b2f73a]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-84b2f73a]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-84b2f73a]{font-size:12px}.vip-sub[data-v-84b2f73a]{font-size:12px;color:#7c3aed}.actions[data-v-84b2f73a]{display:flex;flex-wrap:wrap;gap:8px}

View File

@@ -1 +1 @@
import{S as n}from"./index-CDhtYQo-.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
import{O as n}from"./index-C9w-iZIr.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{S as a}from"./index-CDhtYQo-.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
import{O as a}from"./index-C9w-iZIr.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};

View File

@@ -1 +1 @@
import{S as a}from"./index-CDhtYQo-.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
import{O as a}from"./index-C9w-iZIr.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};

View File

@@ -1 +1 @@
import{S as t}from"./index-CDhtYQo-.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
import{O as t}from"./index-C9w-iZIr.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};

View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-CDhtYQo-.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DiIt7W4Z.css">
<script type="module" crossorigin src="./assets/index-C9w-iZIr.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-_5Ec1Hmd.css">
</head>
<body>
<div id="app"></div>