chore: internal/webui/dist 改为不追踪(构建产物),新增 AGENTS.md

- .gitignore 增加 internal/webui/dist/,构建产物不再纳入版本控制
  (与 web/dist 一致;go:embed 仍依赖磁盘产物,不依赖 git 跟踪)
- 从索引移除已跟踪的 internal/webui/dist(历史提交仍保留旧产物)
- 新增 AGENTS.md:项目目的、架构、容器构建流程(前端 node:20-alpine
  与 Go golang:1.22-alpine 均走 podman)、环境注意事项与编码约定
This commit is contained in:
2026-08-30 17:30:16 +08:00
parent 70c084e7e9
commit 24430f1867
31 changed files with 111 additions and 156 deletions
+1
View File
@@ -8,6 +8,7 @@ coverage.out
web/node_modules/ web/node_modules/
web/dist/ web/dist/
web/.vite/ web/.vite/
internal/webui/dist/
# 运行时产物 # 运行时产物
*.log *.log
+110
View File
@@ -0,0 +1,110 @@
# AGENTS.md — ws_usernode 项目指南
本文件供开发/编码 agent 快速上手:项目目的、架构、构建流程、环境注意事项与编码约定。
详细设计见 `docs/PLAN.md`,接口见 `docs/API.md`,部署见 `docs/DEPLOY.md`
## 项目目的
**服务器用户管理节点**:对服务器上的**外部用户**(非本机管理员)做集中化管理,交付形态为**单个 Go 二进制**(前端产物 `go:embed` 嵌入,单文件分发)。核心能力:
- 系统账号生命周期:创建、禁用、删除、过期与回收;统一 `ext_` 前缀、`external` 组、口令锁定(仅 SSH 密钥登录)
- SSH 公钥自助管理:用户上传/重命名/吊销,实时同步 `authorized_keys`
- 新账号申请 + 管理员审批流(通过后自动建号并邮件通知)
- SMTP 邮件通知:OTP 登录验证码、审批结果、到期提醒、密码重置
- 管理操作审计(append-only,保留 30 天 + 手动/每日定时归档)
部署形态:单机节点 + RESTful API v1;系统账号操作经 sudoers 白名单命令,为后期"控制面 + 多节点 agent"预留。
## 技术栈与目录
| 层 | 技术 |
|---|---|
| 后端 | Go ≥ 1.22 + Gin + GORMSQLite 开发 / MySQL 生产)+ log/slog |
| 前端 | Vue 3 + Vite + TypeScript + Element Plus + Pinia + Vue Router + Axios |
| 构建 | podman 容器构建前端(node:20-alpine),Go 二进制 go:embed 嵌入 |
| 配置 | TOML`config.example.toml`),环境变量覆盖 `USERNODE_<SECTION>_<FIELD>` |
```
cmd/usernode/ CLIserve / migrate / admin / user / help
internal/api/ HTTP handler + 审计 helper
internal/service/ 业务逻辑(user/approval/audit/key/auth/settings/lifecycle
internal/model/ GORM 模型(User/Approval/AuditLog/MailLog/Session/Setting
internal/pkg/ 校验等公共工具(validate.go:用户名/邮箱规则)
internal/auth/ OTP、验证码、会话、密码重置
internal/system/ 系统账号操作层(useradd/usermod/...sudoers 白名单)
internal/webui/ go:embed 前端产物(dist/ 被 git 跟踪)
internal/router/ 路由装配
web/ 前端源码(Vue3)
docs/ PLAN.md / API.md / DEPLOY.md
deploy/ Containerfile(多阶段镜像)、sudoers、systemd 服务
```
## 开发构建流程
**一切构建操作必须在 podman 容器内进行**,不要尝试在宿主机 install / build。
```bash
# 一次性:构建前端 dev 镜像 + 初始化 node_modules volume
make web-install
# 前端热开发(容器挂载源码,5173 端口,API 代理到 Go 后端 8080
make web-dev # 依赖宿主机 Go 后端已启动(make run)
# 标准构建(每次改前端后走这套):
make web-build # 容器内 npm run build → web/dist → 复制到 internal/webui/dist
make build-embed # 重新编译含前端产物的 Go 二进制 → bin/usernode
# 后端
make build # 仅编译 Go(不含前端)
make run # 本地运行(默认 config.toml
make test # go test ./...
make vet
make migrate # 数据库迁移
make image # 多阶段容器镜像(deploy/Containerfile,内含 golang:1.22-alpine 构建阶段)
make clean # 清 bin/ 和 web/dist
```
### 关键细节
- **Go 容器构建方式**:项目标准做法是 `deploy/Containerfile` 多阶段构建(前端 node:20-alpine → Go golang:1.22-alpine → 运行镜像),`make image` 一次产出含前端与后端的镜像。本地开发若要单独产 Go 二进制,应**用与容器一致的 golang:1.22-alpine 环境**`podman run --rm -v "$PWD":/src -w /src golang:1.22-alpine sh -c "CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/usernode ./cmd/usernode"`),避免宿主机 go 1.26.7 版本漂移。
- **`make build-embed` 直接用宿主机 go 编译**(当前 1.26.7),与项目基线(go.mod `go 1.22`)不一致;仅在确认宿主机 go 版本与目标一致时使用,否则改走容器构建。
- **Go 测试/静态检查可用宿主机 go**(`make test` / `make vet`,仅本地开发验证用;正式产物以容器构建为准)。
- **前端构建链**`web/dist`gitignore)→ `internal/webui/dist`(**git 跟踪,必须随改动提交**,否则 index.html 引用的新 hash 文件不入库)。
- **重建前清空 `internal/webui/dist`**`make web-build` 内是 `cp -r` 合并复制,不清旧 hash 文件会导致 `go:embed` 把新旧产物一起打进二进制。标准做法:`rm -rf internal/webui/dist && mkdir -p internal/webui/dist && make web-build`
- **新增 npm 依赖**(在容器内安装,同步宿主机 package.json / package-lock.json):
```bash
podman run --rm --network=host \
-v "$PWD/web:/app" -v ws_usernode_node_modules:/app/node_modules \
ws-usernode-web-dev sh -c "npm install <pkg> --save"
```
- **前端类型检查**(容器内):`podman run --rm --network=host -v "$PWD/web:/app" -v ws_usernode_node_modules:/app/node_modules ws-usernode-web-dev sh -c "npx vue-tsc --noEmit"`。
- **代理环境**:容器构建无法继承宿主机 proxychains。Makefile 经 `--build-arg` 传 `HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY`。若宿主机 LD_PRELOAD 了 proxychainspodman 拉镜像会失败,需:`env -u LD_PRELOAD -u PROXYCHAINS_CONF_FILE make web-build`。
- **git 跟踪约定**`bin/`、`web/dist/`、`web/node_modules/`、`data/`、`config.toml` 均 gitignore`internal/webui/dist/` 例外(跟踪)。
## 环境注意事项(本开发机)
- 环境可能存在代理(proxychains node 等),本地回环可能会被拒绝,尝试绕过。
- 不要在本机装 Node 工具链,一律走 podman。
- 正式 Go 产物请走 `golang:1.22-alpine` 容器(见"Go 容器构建方式"),测试/静态检查可用宿主机 go。
- podman 有镜像 `localhost/ws-usernode-web-dev` 与 volume `ws_usernode_node_modules`(缓存依赖,勿删)。
- 无法访问本地浏览器后端(browser-use 不可用),视觉验证交给用户。
## 编码约定与易错点
- **外部用户名校验**`internal/pkg/validate.go`):申请/创建时传**不含前缀**的用户名,须匹配 `^[a-z][a-z0-9]{1,31}$`(小写开头、2~32 位);最终系统账号 = `user_prefix` + 用户名,总长 ≤ 32。
- **申请页(ApplyView)已改为中文姓名**:前端校验 2~10 个汉字,用 `pinyin-pro``{ toneType:'none', type:'array', surname:'head' }`,surname 处理姓氏多音字如 曾→zeng、单→shan)转拼音作为用户名提交;alert 实时预览 `ext_<username>`。改这里时保持"姓名→拼音→提交"链路与 `surname:'head'` 选项。
- **同名处理**:提交申请时后端 `usernameAvailable` 拦截"用户表已存在 + 同名待审批",返回 `ErrUsernameTaken` → HTTP 409;审批通过时复查用户表(防审批期间被手动创建)。前端 axios 拦截器统一弹出后端 error 文案,失败也进审计。
- **审计 append-only**:业务代码只允许 `AuditService.Record` 写入(禁 Update/Delete);`Result` ∈ success/failed。`AuditFilter.Result` 支持按结果筛选(管理员"错误日志"= 审计页选"失败"),导出 CSV 仅带时间范围(有意为之,勿随意改)。
- **审计清理**`audit.archive_dir` 留空 = 不归档也不清理(防丢审计);生产配置目录后超期审计先归档 CSV 再删除。
- **系统账号操作**`system.dry_run=true` 时只打印命令;生产 `sudo=true` 走 sudoers 白名单(deploy/sudoers),节点以专有用户运行。
- **邮件**:SMTP 留空则禁用邮件;OTP 双通道(邮件 + `usernode user otp --username ext_xxx`)同验证码、同 10 分钟有效期、同 60s 冷却。
- **前端主题**:主题定制集中在 `web/src/styles/theme.css`CSS 变量覆盖 Element Plus+ `web/src/stores/theme.ts`(暗色切换,localStorage key `ws-usernode-theme`);`index.html` 有防暗色闪烁内联脚本。改主题优先改变量而非逐页样式。
- **测试**:后端 `go test ./...`;前端无测试框架,靠 vue-tsc + 构建。
## 其他补充
- 版本号在 Makefile `VERSION`(当前 `0.3.0-m5`),编译期 `-X main.version` 注入。
- 新模型必须注册进 `internal/model` 的模型列表以触发 AutoMigrate。
- git 暂存时仅暂存已知改动,不要直接全部暂存。提交前必须和用户确认,即使在完全访问(YOLO)的模式下也需要先询问用户再提交。
- 数据库迁移:开发可直接 `make migrate`;生产 MySQL 切换参考 `deploy/migrate-sqlite2mysql.sh`。
-1
View File
@@ -1 +0,0 @@
import{d as X,u as Y,v as Z,l as s,w as o,b as t,e as d,a as e,h as a,x as ee,y as oe,k as i,t as r,z as te,A as w,T as ne,B as ae,r as le,C as m,f as se,D as de}from"./index-7syy_I7y.js";import{_ as ue}from"./_plugin-vue_export-helper-DlAUqK2U.js";const _e={class:"logo"},ce={class:"logo-icon"},ie={class:"logo-text"},re={class:"header-left"},me={class:"header-right"},pe={class:"user-chip"},fe=X({__name:"AdminLayout",setup(ve){const g=de(),D=se(),p=Y(),f=Z(),u=le(!1),S=m(()=>g.path),x=m(()=>{var _;return((_=p.me)==null?void 0:_.username)??""}),T=m(()=>{var _;return((_=p.me)==null?void 0:_.email)??""}),v=m(()=>g.meta.title??"");async function A(){await p.logout(),D.push("/login")}return(_,n)=>{const z=t("Monitor"),l=t("el-icon"),M=t("Odometer"),c=t("el-menu-item"),B=t("User"),F=t("Tickets"),N=t("Document"),V=t("Setting"),L=t("el-menu"),U=t("el-aside"),E=t("Expand"),H=t("Fold"),k=t("el-button"),O=t("HomeFilled"),y=t("el-breadcrumb-item"),R=t("el-breadcrumb"),$=t("Sunny"),I=t("Moon"),j=t("el-tooltip"),q=t("el-avatar"),G=t("ArrowDown"),h=t("el-dropdown-item"),J=t("el-dropdown-menu"),K=t("el-dropdown"),P=t("el-header"),Q=t("router-view"),W=t("el-main"),C=t("el-container");return d(),s(C,{class:"admin-layout"},{default:o(()=>[e(U,{width:u.value?"64px":"220px",class:"aside"},{default:o(()=>[a("div",_e,[a("span",ce,[e(l,{size:20},{default:o(()=>[e(z)]),_:1})]),ee(a("span",ie,"用户管理节点",512),[[oe,!u.value]])]),e(L,{"default-active":S.value,collapse:u.value,"collapse-transition":!1,router:"",class:"menu","background-color":"transparent","text-color":"rgba(255,255,255,0.72)","active-text-color":"#ffffff"},{default:o(()=>[e(c,{index:"/admin/dashboard"},{default:o(()=>[e(l,null,{default:o(()=>[e(M)]),_:1}),n[2]||(n[2]=a("span",null,"仪表盘",-1))]),_:1}),e(c,{index:"/admin/users"},{default:o(()=>[e(l,null,{default:o(()=>[e(B)]),_:1}),n[3]||(n[3]=a("span",null,"用户管理",-1))]),_:1}),e(c,{index:"/admin/approvals"},{default:o(()=>[e(l,null,{default:o(()=>[e(F)]),_:1}),n[4]||(n[4]=a("span",null,"申请审批",-1))]),_:1}),e(c,{index:"/admin/audit"},{default:o(()=>[e(l,null,{default:o(()=>[e(N)]),_:1}),n[5]||(n[5]=a("span",null,"审计日志",-1))]),_:1}),e(c,{index:"/admin/settings"},{default:o(()=>[e(l,null,{default:o(()=>[e(V)]),_:1}),n[6]||(n[6]=a("span",null,"系统设置",-1))]),_:1})]),_:1},8,["default-active","collapse"])]),_:1},8,["width"]),e(C,{class:"right"},{default:o(()=>[e(P,{class:"header"},{default:o(()=>[a("div",re,[e(k,{link:"",class:"icon-btn",onClick:n[0]||(n[0]=b=>u.value=!u.value)},{default:o(()=>[e(l,{size:18},{default:o(()=>[u.value?(d(),s(E,{key:0})):(d(),s(H,{key:1}))]),_:1})]),_:1}),e(R,{separator:"/"},{default:o(()=>[e(y,{to:{path:"/admin/dashboard"}},{default:o(()=>[e(l,{class:"crumb-home"},{default:o(()=>[e(O)]),_:1}),n[7]||(n[7]=a("span",null,"首页",-1))]),_:1}),v.value&&v.value!=="仪表盘"?(d(),s(y,{key:0},{default:o(()=>[i(r(v.value),1)]),_:1})):te("",!0)]),_:1})]),a("div",me,[e(j,{content:w(f).isDark?"切换到亮色":"切换到暗色"},{default:o(()=>[e(k,{link:"",class:"icon-btn",onClick:n[1]||(n[1]=b=>w(f).toggle())},{default:o(()=>[e(l,{size:17},{default:o(()=>[w(f).isDark?(d(),s($,{key:0})):(d(),s(I,{key:1}))]),_:1})]),_:1})]),_:1},8,["content"]),e(K,{onCommand:A},{dropdown:o(()=>[e(J,null,{default:o(()=>[e(h,{disabled:""},{default:o(()=>[i(r(T.value),1)]),_:1}),e(h,{divided:"",command:"logout"},{default:o(()=>[...n[8]||(n[8]=[i("退出登录",-1)])]),_:1})]),_:1})]),default:o(()=>[a("span",pe,[e(q,{size:28,class:"avatar"},{default:o(()=>[i(r(x.value.slice(0,1).toUpperCase()),1)]),_:1}),a("span",null,r(x.value),1),e(l,null,{default:o(()=>[e(G)]),_:1})])]),_:1})])]),_:1}),e(W,{class:"main"},{default:o(()=>[e(Q,null,{default:o(({Component:b})=>[e(ne,{name:"fade",mode:"out-in"},{default:o(()=>[(d(),s(ae(b)))]),_:2},1024)]),_:1})]),_:1})]),_:1})]),_:1})}}}),ge=ue(fe,[["__scopeId","data-v-145d606b"]]);export{ge as default};
-1
View File
@@ -1 +0,0 @@
.admin-layout[data-v-145d606b]{min-height:100vh}.aside[data-v-145d606b]{background:var(--app-sidebar-bg);border-right:none;transition:width .2s ease;overflow:hidden;display:flex;flex-direction:column}.logo[data-v-145d606b]{display:flex;align-items:center;gap:10px;height:var(--app-header-height);padding:0 14px;flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.08)}.logo-icon[data-v-145d606b]{width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:#fff;flex-shrink:0;background:linear-gradient(135deg,#6366f1,#8b5cf6);box-shadow:0 4px 12px #6366f166}.logo-text[data-v-145d606b]{font-weight:650;font-size:15px;color:#fff;white-space:nowrap}.menu[data-v-145d606b]{border-right:none;flex:1;padding-top:8px}.aside[data-v-145d606b] .el-menu-item{margin:4px 10px;border-radius:8px;--el-menu-item-height: 44px}.aside[data-v-145d606b] .el-menu-item:hover{background:#ffffff14}.aside[data-v-145d606b] .el-menu-item.is-active{background:var(--app-sidebar-bg-active)}.aside[data-v-145d606b] .el-menu-item.is-active .el-icon{color:var(--app-sidebar-text-active)}.right[data-v-145d606b]{min-width:0}.header[data-v-145d606b]{height:var(--app-header-height);display:flex;align-items:center;justify-content:space-between;gap:16px;background:var(--el-bg-color);border-bottom:1px solid var(--el-border-color-light);box-shadow:0 1px 4px #0f172a0a}.header-left[data-v-145d606b],.header-right[data-v-145d606b]{display:flex;align-items:center;gap:10px}.icon-btn[data-v-145d606b]{color:var(--el-text-color-regular);padding:4px}.crumb-home[data-v-145d606b]{margin-right:4px;vertical-align:-2px}.user-chip[data-v-145d606b]{display:flex;align-items:center;gap:8px;cursor:pointer;outline:none}.avatar[data-v-145d606b]{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff}.main[data-v-145d606b]{padding:20px;background:var(--el-bg-color-page)}
-1
View File
@@ -1 +0,0 @@
import{d as k,c as z,a as l,w as t,b as s,e as E,h as r,k as i,i as q,r as _,m as C,s as U,E as A,f as B}from"./index-7syy_I7y.js";import{_ as N}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P={class:"auth-page"},R={class:"auth-brand"},M={class:"brand-icon"},O={class:"auth-foot"},S=k({__name:"ApplyView",setup(T){B();const p=_(!1),o=C({username:"",email:"",supervisor:"",purpose:""}),f={username:[{required:!0,message:"请输入用户名",trigger:"blur"},{pattern:/^[a-z][a-z0-9_]{1,30}$/,message:"小写字母开头,仅小写字母/数字/下划线,2~31 位",trigger:"blur"}],email:[{required:!0,message:"请输入邮箱",trigger:"blur"},{type:"email",message:"邮箱格式不正确",trigger:"blur"}],supervisor:[{required:!0,message:"请输入挂靠老师",trigger:"blur"}],purpose:[{required:!0,message:"请说明用途",trigger:"blur"}]},m=_();async function d(){var e;if(await((e=m.value)==null?void 0:e.validate().catch(()=>!1))){p.value=!0;try{await U({username:o.username.trim(),email:o.email.trim(),supervisor:o.supervisor.trim(),purpose:o.purpose.trim()}),A.success("申请已提交,等待管理员审批(结果将邮件通知)"),Object.assign(o,{username:"",email:"",supervisor:"",purpose:""})}catch{}finally{p.value=!1}}}return(c,e)=>{const g=s("EditPen"),v=s("el-icon"),b=s("el-alert"),V=s("el-divider"),u=s("el-input"),n=s("el-form-item"),y=s("el-button"),w=s("el-form"),x=s("router-link"),h=s("el-card");return E(),z("div",P,[l(h,{class:"auth-card apply-card",shadow:"never"},{default:t(()=>[r("div",R,[r("span",M,[l(v,{size:26},{default:t(()=>[l(g)]),_:1})]),e[4]||(e[4]=r("h2",null,"申请服务器账号",-1)),e[5]||(e[5]=r("span",{class:"sub"},"提交后由管理员审批,结果以邮件通知",-1))]),l(b,{type:"info",closable:!1,"show-icon":"",class:"apply-tip"},{default:t(()=>[...e[6]||(e[6]=[i(" 通过后自动创建账号(用户名将带 ",-1),r("code",null,"ext_",-1),i(" 前缀)。 ",-1)])]),_:1}),l(V),l(w,{ref_key:"formRef",ref:m,model:o,rules:f,"label-width":"90px",onSubmit:q(d,["prevent"])},{default:t(()=>[l(n,{label:"用户名",prop:"username"},{default:t(()=>[l(u,{modelValue:o.username,"onUpdate:modelValue":e[0]||(e[0]=a=>o.username=a),placeholder:"如 zhangsan(自动成为 ext_zhangsan",maxlength:"31"},null,8,["modelValue"])]),_:1}),l(n,{label:"邮箱",prop:"email"},{default:t(()=>[l(u,{modelValue:o.email,"onUpdate:modelValue":e[1]||(e[1]=a=>o.email=a),placeholder:"接收通知与 OTP 验证码"},null,8,["modelValue"])]),_:1}),l(n,{label:"挂靠老师",prop:"supervisor"},{default:t(()=>[l(u,{modelValue:o.supervisor,"onUpdate:modelValue":e[2]||(e[2]=a=>o.supervisor=a)},null,8,["modelValue"])]),_:1}),l(n,{label:"用途",prop:"purpose"},{default:t(()=>[l(u,{modelValue:o.purpose,"onUpdate:modelValue":e[3]||(e[3]=a=>o.purpose=a),type:"textarea",rows:3,placeholder:"说明申请用途"},null,8,["modelValue"])]),_:1}),l(n,null,{default:t(()=>[l(y,{type:"primary",class:"auth-submit",loading:p.value,onClick:d},{default:t(()=>[...e[7]||(e[7]=[i("提交申请",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),r("div",O,[l(x,{to:"/login"},{default:t(()=>[...e[8]||(e[8]=[i("已有账号?返回登录",-1)])]),_:1})])]),_:1})])}}}),$=N(S,[["__scopeId","data-v-b18f1e34"]]);export{$ as default};
-1
View File
@@ -1 +0,0 @@
.apply-card[data-v-b18f1e34]{width:520px}.apply-tip[data-v-b18f1e34]{margin-bottom:4px}
-1
View File
@@ -1 +0,0 @@
.toolbar[data-v-84a7123e]{display:flex;gap:8px;margin-bottom:14px}
-1
View File
@@ -1 +0,0 @@
import{d as H,o as J,c as h,a as e,w as l,U as G,r as i,b as o,e as m,h as K,k as n,x as L,l as x,t as _,A as j,J as O,z as P,m as Q,R as W,V as B,E as V,H as X}from"./index-7syy_I7y.js";import{b as Y,f as Z}from"./format-BLhrtLRJ.js";import{_ as ee}from"./_plugin-vue_export-helper-DlAUqK2U.js";const te={class:"toolbar"},le=H({__name:"ApprovalsView",setup(ae){const g=i([]),b=i(""),f=i(!1),T={pending:"warning",approved:"success",rejected:"danger"};async function u(){f.value=!0;try{const r=await G(b.value||void 0);g.value=r.items}catch{}finally{f.value=!1}}const d=i(!1),v=i(null),p=Q({reason:""}),y=i(!1);function q(r){v.value=r,p.reason="",d.value=!0}async function A(r){try{await W.confirm(`通过后将为 ${r.username_requested} 自动创建账号(ext_${r.username_requested})并邮件通知,确认?`,"审批通过",{type:"warning",confirmButtonText:"确认通过",cancelButtonText:"取消"})}catch{return}try{await B(r.id,!0,""),V.success("已通过,账号创建中"),u()}catch{}}async function $(){if(v.value){if(!p.reason.trim()){V.warning("拒绝时必须填写理由");return}y.value=!0;try{await B(v.value.id,!1,p.reason.trim()),V.success("已拒绝"),d.value=!1,u()}catch{}finally{y.value=!1}}}return J(u),(r,a)=>{const w=o("el-option"),D=o("el-select"),c=o("el-button"),s=o("el-table-column"),E=o("el-tag"),k=o("el-text"),N=o("el-table"),U=o("el-empty"),z=o("el-card"),C=o("el-form-item"),F=o("el-input"),M=o("el-form"),R=o("el-dialog"),I=X("loading");return m(),h("div",null,[e(z,{shadow:"never"},{default:l(()=>[K("div",te,[e(D,{modelValue:b.value,"onUpdate:modelValue":a[0]||(a[0]=t=>b.value=t),placeholder:"全部状态",clearable:"",style:{width:"150px"},onChange:u},{default:l(()=>[e(w,{label:"待审批",value:"pending"}),e(w,{label:"已通过",value:"approved"}),e(w,{label:"已拒绝",value:"rejected"})]),_:1},8,["modelValue"]),e(c,{type:"primary",onClick:u},{default:l(()=>[...a[4]||(a[4]=[n("刷新",-1)])]),_:1})]),L((m(),x(N,{data:g.value,stripe:""},{default:l(()=>[e(s,{prop:"id",label:"ID",width:"70"}),e(s,{prop:"username_requested",label:"申请用户名",width:"140"},{default:l(({row:t})=>[n("ext_"+_(t.username_requested),1)]),_:1}),e(s,{prop:"email",label:"邮箱","min-width":"180"}),e(s,{prop:"supervisor",label:"挂靠老师",width:"120"}),e(s,{prop:"purpose",label:"用途","min-width":"160","show-overflow-tooltip":""}),e(s,{label:"状态",width:"90"},{default:l(({row:t})=>[e(E,{type:T[t.status]??"info",size:"small"},{default:l(()=>[n(_(j(Y)[t.status]??t.status),1)]),_:2},1032,["type"])]),_:1}),e(s,{label:"提交时间",width:"170"},{default:l(({row:t})=>[n(_(j(Z)(t.created_at)),1)]),_:1}),e(s,{label:"拒绝理由","min-width":"140","show-overflow-tooltip":""},{default:l(({row:t})=>[n(_(t.reason||"-"),1)]),_:1}),e(s,{label:"操作",width:"150",fixed:"right"},{default:l(({row:t})=>[t.status==="pending"?(m(),h(O,{key:0},[e(c,{link:"",type:"success",onClick:S=>A(t)},{default:l(()=>[...a[5]||(a[5]=[n("通过",-1)])]),_:1},8,["onClick"]),e(c,{link:"",type:"danger",onClick:S=>q(t)},{default:l(()=>[...a[6]||(a[6]=[n("拒绝",-1)])]),_:1},8,["onClick"])],64)):(m(),x(k,{key:1,type:"info",size:"small"},{default:l(()=>[...a[7]||(a[7]=[n("已处理",-1)])]),_:1}))]),_:1})]),_:1},8,["data"])),[[I,f.value]]),!f.value&&g.value.length===0?(m(),x(U,{key:0,description:"暂无申请"})):P("",!0)]),_:1}),e(R,{modelValue:d.value,"onUpdate:modelValue":a[3]||(a[3]=t=>d.value=t),title:"拒绝申请",width:"440px"},{footer:l(()=>[e(c,{onClick:a[2]||(a[2]=t=>d.value=!1)},{default:l(()=>[...a[8]||(a[8]=[n("取消",-1)])]),_:1}),e(c,{type:"danger",loading:y.value,onClick:$},{default:l(()=>[...a[9]||(a[9]=[n("确认拒绝",-1)])]),_:1},8,["loading"])]),default:l(()=>[e(M,{"label-width":"80px"},{default:l(()=>[e(C,{label:"用户名"},{default:l(()=>[e(k,null,{default:l(()=>{var t;return[n(_((t=v.value)==null?void 0:t.username_requested),1)]}),_:1})]),_:1}),e(C,{label:"理由",required:""},{default:l(()=>[e(F,{modelValue:p.reason,"onUpdate:modelValue":a[1]||(a[1]=t=>p.reason=t),type:"textarea",rows:3,placeholder:"必填,将邮件通知申请人"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}}}),re=ee(le,[["__scopeId","data-v-84a7123e"]]);export{re as default};
-1
View File
@@ -1 +0,0 @@
import{d as B,o as O,c as E,a as t,w as o,G as K,b as n,r,m as N,e as h,h as g,j as k,k as i,x as P,l as j,t as u,A as w,W as G,H as W}from"./index-7syy_I7y.js";import{a as q,p as F,f as J}from"./format-BLhrtLRJ.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";const Q={class:"toolbar"},R={class:"rid"},X=B({__name:"AuditView",setup(Z){const y=r([]),b=r(0),d=r(1),_=r(20),m=r(!1),l=N({actor:"",action:"",resource_type:"",since:"",until:""}),z={success:"success",failed:"danger"};async function p(){m.value=!0;try{const v=await K({page:d.value,page_size:_.value,actor:l.actor||void 0,action:l.action||void 0,resource_type:l.resource_type||void 0,since:l.since?new Date(l.since).toISOString():void 0,until:l.until?new Date(l.until).toISOString():void 0});y.value=v.items,b.value=v.total}catch{}finally{m.value=!1}}function c(){d.value=1,p()}function C(){window.open(G({since:l.since?new Date(l.since).toISOString():void 0,until:l.until?new Date(l.until).toISOString():void 0}),"_blank")}return O(p),(v,a)=>{const V=n("el-input"),f=n("el-option"),U=n("el-select"),x=n("el-date-picker"),D=n("el-button"),Y=n("Download"),I=n("el-icon"),s=n("el-table-column"),S=n("el-tag"),T=n("el-table"),H=n("el-pagination"),M=n("el-card"),A=W("loading");return h(),E("div",null,[t(M,{shadow:"never"},{default:o(()=>[g("div",Q,[t(V,{modelValue:l.actor,"onUpdate:modelValue":a[0]||(a[0]=e=>l.actor=e),placeholder:"操作者",clearable:"",style:{width:"150px"},onKeyup:k(c,["enter"])},null,8,["modelValue"]),t(V,{modelValue:l.action,"onUpdate:modelValue":a[1]||(a[1]=e=>l.action=e),placeholder:"动作",clearable:"",style:{width:"160px"},onKeyup:k(c,["enter"])},null,8,["modelValue"]),t(U,{modelValue:l.resource_type,"onUpdate:modelValue":a[2]||(a[2]=e=>l.resource_type=e),placeholder:"资源类型",clearable:"",style:{width:"130px"},onChange:c},{default:o(()=>[t(f,{label:"用户",value:"user"}),t(f,{label:"密钥",value:"ssh_key"}),t(f,{label:"申请",value:"approval"})]),_:1},8,["modelValue"]),t(x,{modelValue:l.since,"onUpdate:modelValue":a[3]||(a[3]=e=>l.since=e),type:"datetime",placeholder:"起始时间",style:{width:"190px"},"value-format":"YYYY-MM-DDTHH:mm:ss"},null,8,["modelValue"]),t(x,{modelValue:l.until,"onUpdate:modelValue":a[4]||(a[4]=e=>l.until=e),type:"datetime",placeholder:"结束时间",style:{width:"190px"},"value-format":"YYYY-MM-DDTHH:mm:ss"},null,8,["modelValue"]),t(D,{type:"primary",onClick:c},{default:o(()=>[...a[7]||(a[7]=[i("查询",-1)])]),_:1}),a[9]||(a[9]=g("div",{class:"spacer"},null,-1)),t(D,{type:"success",onClick:C},{default:o(()=>[t(I,null,{default:o(()=>[t(Y)]),_:1}),a[8]||(a[8]=i(" 导出 CSV ",-1))]),_:1})]),P((h(),j(T,{data:y.value,stripe:""},{default:o(()=>[t(s,{prop:"id",label:"ID",width:"80"}),t(s,{label:"动作","min-width":"130"},{default:o(({row:e})=>[i(u(w(q)(e.action)),1)]),_:1}),t(s,{prop:"actor_name",label:"操作者",width:"120"}),t(s,{label:"资源","min-width":"130"},{default:o(({row:e})=>[t(S,{size:"small",type:"info"},{default:o(()=>[i(u(e.resource_type),1)]),_:2},1024),g("span",R,u(e.resource_id),1)]),_:1}),t(s,{label:"详情","min-width":"180","show-overflow-tooltip":""},{default:o(({row:e})=>[i(u(w(F)(e.detail)),1)]),_:1}),t(s,{label:"结果",width:"80"},{default:o(({row:e})=>[t(S,{type:z[e.result]??"info",size:"small"},{default:o(()=>[i(u(e.result==="success"?"成功":"失败"),1)]),_:2},1032,["type"])]),_:1}),t(s,{prop:"ip",label:"IP",width:"130"}),t(s,{label:"时间",width:"170"},{default:o(({row:e})=>[i(u(w(J)(e.created_at)),1)]),_:1})]),_:1},8,["data"])),[[A,m.value]]),t(H,{"current-page":d.value,"onUpdate:currentPage":a[5]||(a[5]=e=>d.value=e),"page-size":_.value,"onUpdate:pageSize":a[6]||(a[6]=e=>_.value=e),total:b.value,"page-sizes":[20,50,100],layout:"total, sizes, prev, pager, next",class:"pager",onCurrentChange:p,onSizeChange:p},null,8,["current-page","page-size","total"])]),_:1})])}}}),le=L(X,[["__scopeId","data-v-d8004883"]]);export{le as default};
-1
View File
@@ -1 +0,0 @@
.toolbar[data-v-d8004883]{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}.spacer[data-v-d8004883]{flex:1}.pager[data-v-d8004883]{margin-top:14px;justify-content:flex-end}.rid[data-v-d8004883]{margin-left:6px;font-size:12px;color:var(--el-text-color-secondary)}
-1
View File
@@ -1 +0,0 @@
.stat-card[data-v-5827ec1e]{cursor:pointer;text-align:center}.stat-num[data-v-5827ec1e]{font-size:28px;font-weight:700}.stat-label[data-v-5827ec1e]{margin-top:4px;color:var(--el-text-color-secondary);font-size:13px}.stat-card.success .stat-num[data-v-5827ec1e]{color:var(--el-color-success)}.stat-card.warning .stat-num[data-v-5827ec1e]{color:var(--el-color-warning)}.stat-card.danger .stat-num[data-v-5827ec1e]{color:var(--el-color-danger)}.stat-card.primary .stat-num[data-v-5827ec1e]{color:var(--el-color-primary)}.stat-card.info .stat-num[data-v-5827ec1e]{color:var(--el-color-info)}.second-row[data-v-5827ec1e]{margin-top:16px}.card-header[data-v-5827ec1e]{display:flex;align-items:center;justify-content:space-between}
-1
View File
@@ -1 +0,0 @@
import{d as N,o as T,F as A,G as S,x as q,c as E,a as s,w as a,r as g,b as u,H as F,e as V,h as l,t as n,A as p,k as _,l as G,z as H,f as I}from"./index-7syy_I7y.js";import{u as M,f as h,a as P}from"./format-BLhrtLRJ.js";import{_ as R}from"./_plugin-vue_export-helper-DlAUqK2U.js";const U={class:"stat-num"},j={class:"stat-num"},J={class:"stat-num"},K={class:"stat-num"},L={class:"stat-num"},O={class:"stat-num"},Q={class:"card-header"},W={class:"card-header"},X=N({__name:"DashboardView",setup(Y){const b=I(),o=g(null),w=g([]),y=g(!0);T(async()=>{try{const[m,t]=await Promise.all([A(),S({page:1,page_size:10})]);o.value=m,w.value=t.items}catch{}finally{y.value=!1}});function v(m){b.push(m?{path:"/admin/users",query:{status:m}}:"/admin/users")}return(m,t)=>{const d=u("el-card"),i=u("el-col"),k=u("el-row"),x=u("el-button"),r=u("el-table-column"),C=u("el-tag"),$=u("el-table"),D=u("el-empty"),B=F("loading");return q((V(),E("div",null,[s(k,{gutter:16},{default:a(()=>[s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card",onClick:t[0]||(t[0]=e=>v())},{default:a(()=>{var e;return[l("div",U,n(((e=o.value)==null?void 0:e.total)??"-"),1),t[8]||(t[8]=l("div",{class:"stat-label"},"用户总数",-1))]}),_:1})]),_:1}),s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card success",onClick:t[1]||(t[1]=e=>v("active"))},{default:a(()=>{var e;return[l("div",j,n(((e=o.value)==null?void 0:e.active)??"-"),1),t[9]||(t[9]=l("div",{class:"stat-label"},"正常",-1))]}),_:1})]),_:1}),s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card warning",onClick:t[2]||(t[2]=e=>v("disabled"))},{default:a(()=>{var e;return[l("div",J,n(((e=o.value)==null?void 0:e.disabled)??"-"),1),t[10]||(t[10]=l("div",{class:"stat-label"},"已禁用",-1))]}),_:1})]),_:1}),s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card danger",onClick:t[3]||(t[3]=e=>v("expired"))},{default:a(()=>{var e;return[l("div",K,n(((e=o.value)==null?void 0:e.expired)??"-"),1),t[11]||(t[11]=l("div",{class:"stat-label"},"已过期",-1))]}),_:1})]),_:1}),s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card primary",onClick:t[4]||(t[4]=e=>p(b).push("/admin/approvals"))},{default:a(()=>{var e;return[l("div",L,n(((e=o.value)==null?void 0:e.pending_approvals)??"-"),1),t[12]||(t[12]=l("div",{class:"stat-label"},"待审批申请",-1))]}),_:1})]),_:1}),s(i,{span:4},{default:a(()=>[s(d,{shadow:"never",class:"stat-card info",onClick:t[5]||(t[5]=e=>v())},{default:a(()=>{var e,c;return[l("div",O,n(((c=(e=o.value)==null?void 0:e.expiring_soon)==null?void 0:c.length)??"-"),1),t[13]||(t[13]=l("div",{class:"stat-label"},"30 天内到期",-1))]}),_:1})]),_:1})]),_:1}),s(k,{gutter:16,class:"second-row"},{default:a(()=>[s(i,{span:12},{default:a(()=>[s(d,{shadow:"never"},{header:a(()=>[l("div",Q,[t[15]||(t[15]=l("b",null,"近期待过期",-1)),s(x,{link:"",type:"primary",onClick:t[6]||(t[6]=e=>p(b).push("/admin/users"))},{default:a(()=>[...t[14]||(t[14]=[_("全部用户",-1)])]),_:1})])]),default:a(()=>{var e,c,z;return[s($,{data:((e=o.value)==null?void 0:e.expiring_soon)??[],size:"small"},{default:a(()=>[s(r,{prop:"username",label:"用户名",width:"150"}),s(r,{label:"状态",width:"90"},{default:a(({row:f})=>[s(C,{type:f.status==="active"?"success":"info",size:"small"},{default:a(()=>[_(n(p(M)[f.status]??f.status),1)]),_:2},1032,["type"])]),_:1}),s(r,{label:"过期时间","min-width":"160"},{default:a(({row:f})=>[_(n(p(h)(f.expire_at)),1)]),_:1})]),_:1},8,["data"]),(z=(c=o.value)==null?void 0:c.expiring_soon)!=null&&z.length?H("",!0):(V(),G(D,{key:0,description:"近 30 天无到期账号","image-size":60}))]}),_:1})]),_:1}),s(i,{span:12},{default:a(()=>[s(d,{shadow:"never"},{header:a(()=>[l("div",W,[t[17]||(t[17]=l("b",null,"最近审计",-1)),s(x,{link:"",type:"primary",onClick:t[7]||(t[7]=e=>p(b).push("/admin/audit"))},{default:a(()=>[...t[16]||(t[16]=[_("全部审计",-1)])]),_:1})])]),default:a(()=>[s($,{data:w.value,size:"small"},{default:a(()=>[s(r,{label:"动作","min-width":"130"},{default:a(({row:e})=>[_(n(p(P)(e.action)),1)]),_:1}),s(r,{prop:"actor_name",label:"操作者",width:"120"}),s(r,{label:"结果",width:"80"},{default:a(({row:e})=>[s(C,{type:e.result==="success"?"success":"danger",size:"small"},{default:a(()=>[_(n(e.result==="success"?"成功":"失败"),1)]),_:2},1032,["type"])]),_:1}),s(r,{label:"时间","min-width":"160"},{default:a(({row:e})=>[_(n(p(h)(e.created_at)),1)]),_:1})]),_:1},8,["data"])]),_:1})]),_:1})]),_:1})])),[[B,y.value]])}}}),st=R(X,[["__scopeId","data-v-5827ec1e"]]);export{st as default};
-1
View File
@@ -1 +0,0 @@
import{d as j,o as I,c as B,a as l,w as t,r as u,Z as J,b as n,e as c,x as O,l as C,k as o,t as A,A as Z,J as G,z as L,h as z,m as Q,E as _,_ as W,$ as X,R as Y,a0 as ee,H as le}from"./index-7syy_I7y.js";import{f as te}from"./format-BLhrtLRJ.js";import{_ as ae}from"./_plugin-vue_export-helper-DlAUqK2U.js";const ne={class:"card-header"},oe=j({__name:"KeysView",setup(se){const v=u([]),y=u(!1),m=u(!1),k=u(!1),s=Q({name:"",public_key:""});async function g(){y.value=!0;try{const i=await J();v.value=i.items}catch{}finally{y.value=!1}}async function K(){if(!s.name.trim()){_.warning("请填写密钥名称");return}if(!s.public_key.trim()){_.warning("请粘贴公钥内容");return}k.value=!0;try{await W({name:s.name.trim(),public_key:s.public_key.trim()}),_.success("上传成功,已同步到 authorized_keys"),m.value=!1,Object.assign(s,{name:"",public_key:""}),g()}catch{}finally{k.value=!1}}const p=u(!1),b=u(null),f=u(""),V=u(!1);function U(i){b.value=i,f.value=i.name,p.value=!0}async function H(){if(!(!b.value||!f.value.trim())){V.value=!0;try{await X(b.value.id,f.value.trim()),_.success("已重命名"),p.value=!1,g()}catch{}finally{V.value=!1}}}async function N(i){try{await Y.confirm(`吊销密钥「${i.name}」(${i.fingerprint})后将立即从 authorized_keys 移除,SSH 登录立即失效。确认?`,"吊销密钥",{type:"warning",confirmButtonText:"确认吊销",cancelButtonText:"取消"})}catch{return}try{await ee(i.id),_.success("已吊销"),g()}catch{}}return I(g),(i,e)=>{const T=n("Plus"),E=n("el-icon"),r=n("el-button"),d=n("el-table-column"),M=n("el-tag"),R=n("el-text"),h=n("el-table"),D=n("el-empty"),q=n("el-card"),w=n("el-input"),x=n("el-form-item"),S=n("el-form"),$=n("el-dialog"),F=le("loading");return c(),B("div",null,[l(q,{shadow:"never"},{header:t(()=>[z("div",ne,[z("b",null,"我的 SSH 公钥("+A(v.value.length)+"",1),l(r,{type:"primary",size:"small",onClick:e[0]||(e[0]=a=>m.value=!0)},{default:t(()=>[l(E,null,{default:t(()=>[l(T)]),_:1}),e[8]||(e[8]=o(" 上传公钥 ",-1))]),_:1})])]),default:t(()=>[O((c(),C(h,{data:v.value,stripe:""},{default:t(()=>[l(d,{prop:"name",label:"名称","min-width":"140"}),l(d,{prop:"key_type",label:"类型",width:"120"}),l(d,{prop:"fingerprint",label:"指纹","min-width":"200","show-overflow-tooltip":""}),l(d,{label:"状态",width:"90"},{default:t(({row:a})=>[l(M,{type:a.status==="active"?"success":"danger",size:"small"},{default:t(()=>[o(A(a.status==="active"?"生效":"已吊销"),1)]),_:2},1032,["type"])]),_:1}),l(d,{label:"上传时间",width:"170"},{default:t(({row:a})=>[o(A(Z(te)(a.created_at)),1)]),_:1}),l(d,{label:"操作",width:"140",fixed:"right"},{default:t(({row:a})=>[a.status==="active"?(c(),B(G,{key:0},[l(r,{link:"",type:"primary",onClick:P=>U(a)},{default:t(()=>[...e[9]||(e[9]=[o("重命名",-1)])]),_:1},8,["onClick"]),l(r,{link:"",type:"danger",onClick:P=>N(a)},{default:t(()=>[...e[10]||(e[10]=[o("吊销",-1)])]),_:1},8,["onClick"])],64)):(c(),C(R,{key:1,type:"info",size:"small"},{default:t(()=>[...e[11]||(e[11]=[o("已吊销",-1)])]),_:1}))]),_:1})]),_:1},8,["data"])),[[F,y.value]]),!y.value&&v.value.length===0?(c(),C(D,{key:0,description:"暂无密钥,上传公钥后即可 SSH 登录"})):L("",!0)]),_:1}),l($,{modelValue:m.value,"onUpdate:modelValue":e[4]||(e[4]=a=>m.value=a),title:"上传 SSH 公钥",width:"520px"},{footer:t(()=>[l(r,{onClick:e[3]||(e[3]=a=>m.value=!1)},{default:t(()=>[...e[12]||(e[12]=[o("取消",-1)])]),_:1}),l(r,{type:"primary",loading:k.value,onClick:K},{default:t(()=>[...e[13]||(e[13]=[o("上传",-1)])]),_:1},8,["loading"])]),default:t(()=>[l(S,{"label-width":"80px"},{default:t(()=>[l(x,{label:"名称",required:""},{default:t(()=>[l(w,{modelValue:s.name,"onUpdate:modelValue":e[1]||(e[1]=a=>s.name=a),placeholder:"如 我的笔记本",maxlength:"64"},null,8,["modelValue"])]),_:1}),l(x,{label:"公钥",required:""},{default:t(()=>[l(w,{modelValue:s.public_key,"onUpdate:modelValue":e[2]||(e[2]=a=>s.public_key=a),type:"textarea",rows:5,placeholder:"ssh-ed25519 AAAA... 或 ssh-rsa AAAA..."},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"]),l($,{modelValue:p.value,"onUpdate:modelValue":e[7]||(e[7]=a=>p.value=a),title:"重命名密钥",width:"400px"},{footer:t(()=>[l(r,{onClick:e[6]||(e[6]=a=>p.value=!1)},{default:t(()=>[...e[14]||(e[14]=[o("取消",-1)])]),_:1}),l(r,{type:"primary",loading:V.value,onClick:H},{default:t(()=>[...e[15]||(e[15]=[o("保存",-1)])]),_:1},8,["loading"])]),default:t(()=>[l(S,{"label-width":"80px"},{default:t(()=>[l(x,{label:"名称"},{default:t(()=>[l(w,{modelValue:f.value,"onUpdate:modelValue":e[5]||(e[5]=a=>f.value=a),maxlength:"64"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}}}),de=ae(oe,[["__scopeId","data-v-9032698c"]]);export{de as default};
-1
View File
@@ -1 +0,0 @@
.card-header[data-v-9032698c]{display:flex;align-items:center;justify-content:space-between}
-1
View File
@@ -1 +0,0 @@
.inline-row[data-v-273ff0da]{display:flex;gap:8px;width:100%}.otp-row .otp-input[data-v-273ff0da]{flex:1;min-width:0}.captcha-img[data-v-273ff0da]{width:110px;height:32px;border:1px solid var(--el-border-color);border-radius:6px;cursor:pointer;flex-shrink:0}[data-v-273ff0da] .el-tabs{margin-top:4px}
-1
View File
@@ -1 +0,0 @@
import{d as D,u as H,o as R,c as $,a,w as o,r as p,g as G,b as n,e as C,f as J,h as r,i as O,j as k,k as f,l as B,t as Q,m as K,E as g,n as W,p as X,q as Y}from"./index-7syy_I7y.js";import{_ as Z}from"./_plugin-vue_export-helper-DlAUqK2U.js";const ee={class:"auth-page"},ae={class:"auth-brand"},oe={class:"brand-icon"},te={class:"inline-row"},le={class:"inline-row otp-row"},ne={class:"auth-foot"},se=D({__name:"LoginView",setup(ue){const L=J(),x=H(),v=p("admin"),c=p(!1),h=p(!1),u=K({username:"",password:""}),t=K({username:"",code:"",captcha_id:"",captcha_code:""}),s=p(null),b=p(!1),d=p(0);let S;async function i(){b.value=!0;try{s.value=await G()}catch{s.value=null}finally{b.value=!1}}function E(U){d.value=U,S=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(S),d.value=0)},1e3)}async function y(){if(!u.username||!u.password){g.warning("请输入用户名和密码");return}c.value=!0;try{await W(u),await x.fetchMe(),L.push("/admin/dashboard")}catch{}finally{c.value=!1}}async function M(){if(!t.username||!t.captcha_code){g.warning("请输入用户名和图形验证码");return}if(!(!s.value&&(await i(),!s.value))){h.value=!0;try{await X({username:t.username,captcha_id:s.value.captcha_id,captcha_code:t.captcha_code}),g.success("验证码已发送(邮件或 CLIusernode user otp"),E(60),t.captcha_code="",await i()}catch{await i()}finally{h.value=!1}}}async function V(){if(!t.username||!t.code){g.warning("请输入用户名和 OTP 验证码");return}c.value=!0;try{await Y({username:t.username,code:t.code}),await x.fetchMe(),L.push("/me/profile")}catch{}finally{c.value=!1}}function N(){v.value==="user"&&!s.value&&i()}return R(()=>{v.value==="user"&&i()}),(U,e)=>{const P=n("Monitor"),A=n("el-icon"),m=n("el-input"),_=n("el-form-item"),w=n("el-button"),T=n("el-form"),I=n("el-tab-pane"),F=n("el-image"),j=n("el-tabs"),q=n("router-link"),z=n("el-card");return C(),$("div",ee,[a(z,{class:"auth-card",shadow:"never"},{default:o(()=>[r("div",ae,[r("span",oe,[a(A,{size:26},{default:o(()=>[a(P)]),_:1})]),e[6]||(e[6]=r("h2",null,"服务器用户管理节点",-1)),e[7]||(e[7]=r("span",{class:"sub"},"账号申请 · SSH 密钥 · 使用审计",-1))]),a(j,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=l=>v.value=l),stretch:"",onTabChange:N},{default:o(()=>[a(I,{label:"管理员登录",name:"admin"},{default:o(()=>[a(T,{"label-position":"top",onSubmit:O(y,["prevent"])},{default:o(()=>[a(_,{label:"用户名"},{default:o(()=>[a(m,{modelValue:u.username,"onUpdate:modelValue":e[0]||(e[0]=l=>u.username=l),placeholder:"管理员用户名",autocomplete:"username"},null,8,["modelValue"])]),_:1}),a(_,{label:"密码"},{default:o(()=>[a(m,{modelValue:u.password,"onUpdate:modelValue":e[1]||(e[1]=l=>u.password=l),type:"password",placeholder:"密码","show-password":"",autocomplete:"current-password",onKeyup:k(y,["enter"])},null,8,["modelValue"])]),_:1}),a(w,{type:"primary",class:"auth-submit",loading:c.value,onClick:y},{default:o(()=>[...e[8]||(e[8]=[f("登录",-1)])]),_:1},8,["loading"])]),_:1})]),_:1}),a(I,{label:"外部用户登录",name:"user"},{default:o(()=>[a(T,{"label-position":"top",onSubmit:O(V,["prevent"])},{default:o(()=>[a(_,{label:"用户名"},{default:o(()=>[a(m,{modelValue:t.username,"onUpdate:modelValue":e[2]||(e[2]=l=>t.username=l),placeholder:"用户名(可带或不带 ext_ 前缀)",autocomplete:"username"},null,8,["modelValue"])]),_:1}),a(_,{label:"图形验证码"},{default:o(()=>[r("div",te,[a(m,{modelValue:t.captcha_code,"onUpdate:modelValue":e[3]||(e[3]=l=>t.captcha_code=l),placeholder:"4 位验证码",maxlength:"4",onKeyup:k(M,["enter"])},null,8,["modelValue"]),s.value?(C(),B(F,{key:0,src:s.value.image,fit:"contain",class:"captcha-img",title:"点击刷新",onClick:i},null,8,["src"])):(C(),B(w,{key:1,loading:b.value,onClick:i},{default:o(()=>[...e[9]||(e[9]=[f("获取",-1)])]),_:1},8,["loading"]))])]),_:1}),a(_,{label:"登录验证码(OTP"},{default:o(()=>[r("div",le,[a(m,{modelValue:t.code,"onUpdate:modelValue":e[4]||(e[4]=l=>t.code=l),placeholder:"6 位 OTP(邮件或 CLI 获取)",maxlength:"6",class:"otp-input",onKeyup:k(V,["enter"])},null,8,["modelValue"]),a(w,{loading:h.value,disabled:d.value>0,onClick:M},{default:o(()=>[f(Q(d.value>0?`${d.value}s 后重发`:"发送验证码"),1)]),_:1},8,["loading","disabled"])])]),_:1}),a(w,{type:"primary",class:"auth-submit",loading:c.value,onClick:V},{default:o(()=>[...e[10]||(e[10]=[f("登录",-1)])]),_:1},8,["loading"])]),_:1})]),_:1})]),_:1},8,["modelValue"]),r("div",ne,[a(q,{to:"/apply"},{default:o(()=>[...e[11]||(e[11]=[f("还没有账号?申请新账号",-1)])]),_:1})])]),_:1})])}}}),re=Z(se,[["__scopeId","data-v-273ff0da"]]);export{re as default};
-1
View File
@@ -1 +0,0 @@
.pager[data-v-5c05de0c]{margin-top:14px;justify-content:flex-end}
-1
View File
@@ -1 +0,0 @@
import{d as z,o as C,c as V,a,w as t,a1 as k,b as s,r as n,e as g,x as A,l as B,k as i,t as r,A as m,h as D,H as S}from"./index-7syy_I7y.js";import{a as M,p as N,f as T}from"./format-BLhrtLRJ.js";import{_ as I}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P=z({__name:"MyAuditView",setup(U){const f=n([]),v=n(0),c=n(1),u=n(20),d=n(!1);async function p(){d.value=!0;try{const _=await k(c.value,u.value);f.value=_.items,v.value=_.total}catch{}finally{d.value=!1}}return C(p),(_,l)=>{const o=s("el-table-column"),w=s("el-tag"),b=s("el-table"),y=s("el-pagination"),h=s("el-card"),x=S("loading");return g(),V("div",null,[a(h,{shadow:"never"},{header:t(()=>[...l[2]||(l[2]=[D("b",null,"我的审计痕迹",-1)])]),default:t(()=>[A((g(),B(b,{data:f.value,stripe:""},{default:t(()=>[a(o,{label:"动作","min-width":"140"},{default:t(({row:e})=>[i(r(m(M)(e.action)),1)]),_:1}),a(o,{label:"详情","min-width":"200","show-overflow-tooltip":""},{default:t(({row:e})=>[i(r(m(N)(e.detail)),1)]),_:1}),a(o,{label:"结果",width:"80"},{default:t(({row:e})=>[a(w,{type:e.result==="success"?"success":"danger",size:"small"},{default:t(()=>[i(r(e.result==="success"?"成功":"失败"),1)]),_:2},1032,["type"])]),_:1}),a(o,{prop:"ip",label:"IP",width:"130"}),a(o,{label:"时间",width:"170"},{default:t(({row:e})=>[i(r(m(T)(e.created_at)),1)]),_:1})]),_:1},8,["data"])),[[x,d.value]]),a(y,{"current-page":c.value,"onUpdate:currentPage":l[0]||(l[0]=e=>c.value=e),"page-size":u.value,"onUpdate:pageSize":l[1]||(l[1]=e=>u.value=e),total:v.value,"page-sizes":[10,20,50],layout:"total, sizes, prev, pager, next",class:"pager",onCurrentChange:p,onSizeChange:p},null,8,["current-page","page-size","total"])]),_:1})])}}}),q=I(P,[["__scopeId","data-v-5c05de0c"]]);export{q as default};
-1
View File
@@ -1 +0,0 @@
.profile-card[data-v-d8b0b471]{max-width:640px}.notice[data-v-d8b0b471]{margin-top:16px}.soon-tag[data-v-d8b0b471]{margin-left:8px}
-1
View File
@@ -1 +0,0 @@
import{d as B,u as N,o as T,I as D,x as H,c as f,a as t,w as a,r as v,C as A,b as r,H as I,e as u,J as P,k as l,t as o,A as i,h as b,l as d,z as y}from"./index-7syy_I7y.js";import{u as E,f as g,d as x}from"./format-BLhrtLRJ.js";import{_ as F}from"./_plugin-vue_export-helper-DlAUqK2U.js";const J=B({__name:"ProfileView",setup(L){const k=N(),e=v(null),_=v(!0),w=A(()=>{var c;return((c=k.me)==null?void 0:c.username)??""}),S={active:"success",disabled:"info",expired:"danger"};return T(async()=>{try{const n=(await D({page_size:100})).items.find(s=>s.username===w.value)??null;e.value=n}catch{}finally{_.value=!1}}),(c,n)=>{const s=r("el-descriptions-item"),p=r("el-tag"),V=r("el-descriptions"),m=r("el-alert"),h=r("el-empty"),C=r("el-card"),z=I("loading");return H((u(),f("div",null,[t(C,{shadow:"never",class:"profile-card"},{header:a(()=>[...n[0]||(n[0]=[b("b",null,"账号信息",-1)])]),default:a(()=>[e.value?(u(),f(P,{key:0},[t(V,{column:1,border:""},{default:a(()=>[t(s,{label:"用户名"},{default:a(()=>[l(o(e.value.username),1)]),_:1}),t(s,{label:"邮箱"},{default:a(()=>[l(o(e.value.email),1)]),_:1}),t(s,{label:"挂靠老师"},{default:a(()=>[l(o(e.value.supervisor||"-"),1)]),_:1}),t(s,{label:"用途"},{default:a(()=>[l(o(e.value.purpose||"-"),1)]),_:1}),t(s,{label:"状态"},{default:a(()=>[t(p,{type:S[e.value.status]??"info",size:"small"},{default:a(()=>[l(o(i(E)[e.value.status]??e.value.status),1)]),_:1},8,["type"])]),_:1}),t(s,{label:"过期时间"},{default:a(()=>[b("span",null,o(i(g)(e.value.expire_at)),1),e.value.status==="active"&&(i(x)(e.value.expire_at)??999)<=30?(u(),d(p,{key:0,type:"warning",size:"small",class:"soon-tag"},{default:a(()=>[l(" 剩余 "+o(i(x)(e.value.expire_at))+" 天 ",1)]),_:1})):y("",!0)]),_:1}),t(s,{label:"最近登录"},{default:a(()=>[l(o(i(g)(e.value.last_login_at)),1)]),_:1}),t(s,{label:"Shell"},{default:a(()=>[l(o(e.value.shell),1)]),_:1})]),_:1}),e.value.status==="expired"?(u(),d(m,{key:0,type:"error",closable:!1,class:"notice"},{default:a(()=>[...n[1]||(n[1]=[l(" 账号已过期。请在回收期内联系管理员办理延期,否则将被自动回收删除。 ",-1)])]),_:1})):y("",!0),t(m,{type:"info",closable:!1,class:"notice"},{default:a(()=>[...n[2]||(n[2]=[l(" 系统账号口令已锁定(仅支持 SSH 密钥登录)。请到「我的密钥」上传公钥后即可 SSH 登录。 ",-1)])]),_:1})],64)):(u(),d(h,{key:1,description:"未找到账号信息"}))]),_:1})])),[[z,_.value]])}}}),q=F(J,[["__scopeId","data-v-d8b0b471"]]);export{q as default};
-1
View File
@@ -1 +0,0 @@
.settings-table[data-v-90a30993],.notice[data-v-90a30993]{margin-top:16px}
-1
View File
@@ -1 +0,0 @@
import{d as D,o as E,c as M,x as N,l as _,w as e,r,X as I,b as o,H as K,e as d,a as n,k as s,t as p,h as m,C as T,Y as U,E as H}from"./index-7syy_I7y.js";import{_ as P}from"./_plugin-vue_export-helper-DlAUqK2U.js";const X=D({__name:"SettingsView",setup(Y){const f=r([]),u=r(!1),c=r(""),y={"policy.default_ttl":{label:"默认有效期",hint:"新账号默认有效期(如 2160h = 90 天)"},"policy.recycle_period":{label:"回收期",hint:"到期后保留时长,期内可延期恢复(如 720h = 30 天)"},"policy.audit_retention":{label:"审计保留",hint:"审计日志保留时长,超期先归档再清理(如 720h)"}},k=T(()=>f.value.filter(a=>a.key in y).map(a=>({...a,...y[a.key]})));async function v(){u.value=!0;try{const a=await I();f.value=a.items}catch{}finally{u.value=!1}}async function h(a){c.value=a.key;try{await U(a.key,a.value.trim()),H.success("已保存并生效"),await v()}catch{}finally{c.value=""}}return E(v),(a,t)=>{const w=o("el-text"),i=o("el-table-column"),b=o("el-tag"),x=o("el-input"),V=o("el-button"),S=o("el-table"),C=o("el-alert"),B=o("el-card"),z=K("loading");return d(),M("div",null,[N((d(),_(B,{shadow:"never"},{header:e(()=>[...t[0]||(t[0]=[m("b",null,"系统设置",-1)])]),default:e(()=>[n(w,{type:"info"},{default:e(()=>[...t[1]||(t[1]=[s("以下策略通过 settings 表动态覆盖配置文件默认值,保存后立即生效。",-1)])]),_:1}),n(S,{data:k.value,stripe:"",class:"settings-table"},{default:e(()=>[n(i,{label:"配置项",width:"140"},{default:e(({row:l})=>[s(p(l.label),1)]),_:1}),n(i,{label:"Key",width:"220"},{default:e(({row:l})=>[m("code",null,p(l.key),1)]),_:1}),n(i,{label:"当前值"},{default:e(({row:l})=>[n(x,{modelValue:l.value,"onUpdate:modelValue":g=>l.value=g,placeholder:"时长格式,如 2160h"},{suffix:e(()=>[l.overridden?(d(),_(b,{key:0,type:"warning",size:"small"},{default:e(()=>[...t[2]||(t[2]=[s("已覆盖",-1)])]),_:1})):(d(),_(b,{key:1,type:"info",size:"small"},{default:e(()=>[...t[3]||(t[3]=[s("默认",-1)])]),_:1}))]),_:2},1032,["modelValue","onUpdate:modelValue"])]),_:1}),n(i,{label:"说明","min-width":"240"},{default:e(({row:l})=>[s(p(l.hint),1)]),_:1}),n(i,{label:"操作",width:"100"},{default:e(({row:l})=>[n(V,{type:"primary",size:"small",loading:c.value===l.key,onClick:g=>h(l)},{default:e(()=>[...t[4]||(t[4]=[s("保存",-1)])]),_:1},8,["loading","onClick"])]),_:1})]),_:1},8,["data"]),n(C,{type:"info",closable:!1,class:"notice"},{default:e(()=>[...t[5]||(t[5]=[s(" 提示:SMTP、会话时长等参数在 ",-1),m("code",null,"config.toml",-1),s(" 中配置(或环境变量覆盖),不在本页修改。 ",-1)])]),_:1})]),_:1})),[[z,u.value]])])}}}),q=P(X,[["__scopeId","data-v-90a30993"]]);export{q as default};
-1
View File
@@ -1 +0,0 @@
.user-layout[data-v-a89182e3]{min-height:100vh}.header[data-v-a89182e3]{height:var(--app-header-height);display:flex;align-items:center;gap:20px;background:var(--el-bg-color);border-bottom:1px solid var(--el-border-color-light);box-shadow:0 1px 4px #0f172a0a}.logo[data-v-a89182e3]{display:flex;align-items:center;gap:10px;flex-shrink:0}.logo-icon[data-v-a89182e3]{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:#fff;background:linear-gradient(135deg,#6366f1,#8b5cf6);box-shadow:0 4px 12px #6366f166}.logo-text[data-v-a89182e3]{font-weight:650;font-size:15px}.nav[data-v-a89182e3]{flex:1;border-bottom:none}.header-right[data-v-a89182e3]{display:flex;align-items:center;gap:10px;flex-shrink:0}.icon-btn[data-v-a89182e3]{color:var(--el-text-color-regular);padding:4px}.user-chip[data-v-a89182e3]{display:flex;align-items:center;gap:8px;cursor:pointer;outline:none}.avatar[data-v-a89182e3]{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff}.main[data-v-a89182e3]{padding:20px;background:var(--el-bg-color-page)}
-1
View File
@@ -1 +0,0 @@
import{d as I,u as $,v as j,l,w as e,b as t,e as _,a as o,h as s,k as a,A as i,t as v,T as q,B as E,C as w,f as F,D as G}from"./index-7syy_I7y.js";import{_ as H}from"./_plugin-vue_export-helper-DlAUqK2U.js";const J={class:"logo"},K={class:"logo-icon"},O={class:"header-right"},P={class:"user-chip"},Q=I({__name:"UserLayout",setup(W){const g=G(),y=F(),m=$(),u=j(),k=w(()=>g.path),p=w(()=>{var r;return((r=m.me)==null?void 0:r.username)??""});async function x(){await m.logout(),y.push("/login")}return(r,n)=>{const h=t("Monitor"),c=t("el-icon"),d=t("el-menu-item"),C=t("el-menu"),D=t("Sunny"),M=t("Moon"),S=t("el-button"),b=t("el-tooltip"),z=t("el-avatar"),A=t("ArrowDown"),B=t("el-dropdown-item"),T=t("el-dropdown-menu"),L=t("el-dropdown"),N=t("el-header"),U=t("router-view"),V=t("el-main"),R=t("el-container");return _(),l(R,{class:"user-layout"},{default:e(()=>[o(N,{class:"header"},{default:e(()=>[s("span",J,[s("span",K,[o(c,{size:18},{default:e(()=>[o(h)]),_:1})]),n[1]||(n[1]=s("span",{class:"logo-text"},"服务器用户管理",-1))]),o(C,{"default-active":k.value,router:"",mode:"horizontal",ellipsis:!1,class:"nav"},{default:e(()=>[o(d,{index:"/me/profile"},{default:e(()=>[...n[2]||(n[2]=[a("账号信息",-1)])]),_:1}),o(d,{index:"/me/keys"},{default:e(()=>[...n[3]||(n[3]=[a("我的密钥",-1)])]),_:1}),o(d,{index:"/me/audit"},{default:e(()=>[...n[4]||(n[4]=[a("我的审计",-1)])]),_:1})]),_:1},8,["default-active"]),s("div",O,[o(b,{content:i(u).isDark?"切换到亮色":"切换到暗色"},{default:e(()=>[o(S,{link:"",class:"icon-btn",onClick:n[0]||(n[0]=f=>i(u).toggle())},{default:e(()=>[o(c,{size:17},{default:e(()=>[i(u).isDark?(_(),l(D,{key:0})):(_(),l(M,{key:1}))]),_:1})]),_:1})]),_:1},8,["content"]),o(L,{onCommand:x},{dropdown:e(()=>[o(T,null,{default:e(()=>[o(B,{command:"logout"},{default:e(()=>[...n[5]||(n[5]=[a("退出登录",-1)])]),_:1})]),_:1})]),default:e(()=>[s("span",P,[o(z,{size:26,class:"avatar"},{default:e(()=>[a(v(p.value.slice(0,1).toUpperCase()),1)]),_:1}),s("span",null,v(p.value),1),o(c,null,{default:e(()=>[o(A)]),_:1})])]),_:1})])]),_:1}),o(V,{class:"main"},{default:e(()=>[o(U,null,{default:e(({Component:f})=>[o(q,{name:"fade",mode:"out-in"},{default:e(()=>[(_(),l(E(f)))]),_:2},1024)]),_:1})]),_:1})]),_:1})}}}),Z=H(Q,[["__scopeId","data-v-a89182e3"]]);export{Z as default};
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.toolbar[data-v-e4076ba9]{display:flex;gap:8px;margin-bottom:14px}.spacer[data-v-e4076ba9]{flex:1}.pager[data-v-e4076ba9]{margin-top:14px;justify-content:flex-end}.soon-tag[data-v-e4076ba9]{margin-left:6px}.actions[data-v-e4076ba9]{margin-top:16px;display:flex;gap:8px;flex-wrap:wrap}.tip[data-v-e4076ba9]{font-size:12px;color:var(--el-text-color-secondary)}
@@ -1 +0,0 @@
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};
-1
View File
@@ -1 +0,0 @@
function a(e){if(!e)return"-";const t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString("zh-CN",{hour12:!1})}function n(e){if(!e)return null;const t=new Date(e).getTime()-Date.now();return Math.ceil(t/864e5)}const u={active:"正常",disabled:"已禁用",expired:"已过期"},s={pending:"待审批",approved:"已通过",rejected:"已拒绝"},r={"auth.admin_login":"管理员登录","auth.otp_send":"发送 OTP","auth.otp_login":"OTP 登录","auth.logout":"登出","user.create":"创建用户","user.update":"更新用户","user.disable":"禁用用户","user.enable":"启用用户","user.extend":"延期用户","user.delete":"删除用户","key.create":"上传密钥","key.rename":"重命名密钥","key.revoke":"吊销密钥","approval.submit":"提交申请","approval.review":"审批申请","lifecycle.expire":"到期锁定","lifecycle.recycle":"回收删除"};function i(e){return r[e]||e}function o(e){try{return JSON.stringify(JSON.parse(e))}catch{return e}}export{i as a,s as b,n as d,a as f,o as p,u};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-28
View File
@@ -1,28 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%234f46e5'/%3E%3Crect x='7' y='7' width='18' height='13' rx='2' fill='none' stroke='white' stroke-width='2.4'/%3E%3Cline x1='9' y1='13' x2='23' y2='13' stroke='white' stroke-width='2'/%3E%3Cline x1='14' y1='20' x2='14' y2='24' stroke='white' stroke-width='2.4'/%3E%3Cline x1='18' y1='20' x2='18' y2='24' stroke='white' stroke-width='2.4'/%3E%3Cline x1='11' y1='24' x2='21' y2='24' stroke='white' stroke-width='2.4'/%3E%3C/svg%3E"
/>
<title>ws_usernode 用户管理</title>
<script>
// 在渲染前恢复主题,避免暗色模式闪白
;(function () {
try {
var t = localStorage.getItem('ws-usernode-theme')
if (t === 'dark' || (t !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
} catch (e) {}
})()
</script>
<script type="module" crossorigin src="/assets/index-7syy_I7y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Zt5ivs7P.css">
</head>
<body>
<div id="app"></div>
</body>
</html>