09f6918aeb
Schema:
- New AdminUser model with bcrypt-hashed password (cost 10)
- Roles: admin (full) / operator (read-only ops)
- Status: active / disabled
- MustChangePassword flag forces first-login password change
Backend:
- store: Add admins [] + CRUD methods (ListAdmins strips PasswordHash)
- service: SeedDefaultAdminIfEmpty (uses env credentials on first run),
CreateAdmin, ChangePassword, ResetPassword, SetAdminStatus, DeleteAdmin
- middleware: JWT now carries user_id (UUID)
- api: login() uses bcrypt + updates last_login_at/ip, blocks disabled
- api: me() returns role + must_change_password
- api: new endpoints:
POST /api/me/password (self password change)
GET /api/admins
POST /api/admins (create)
POST /api/admins/:id/password (reset by admin)
POST /api/admins/:id/status (enable/disable)
DELETE /api/admins/:id (with self/last-admin guard)
Frontend:
- Login: must_change_password=true triggers forced change-password dialog
- Layout: admin dropdown shows role tag + 修改密码 / 退出登录
- New /admins page (admin only) with table + create/reset/status/delete
- Router guard hides /admins from non-admin accounts
- API client: Auth.changePassword, Admins.{list,create,resetPassword,setStatus,delete}
Security:
- PasswordHash stored as bcrypt $2a$10$... in db.json
- ListAdmins always returns PasswordHash=''; never leaks via API
- Login returns 403 for disabled accounts
Verified: 21/21 API tests + browser E2E (first-login forced change,
restart persistence, admin list without hash, role-based menu)
122 lines
5.4 KiB
Go
122 lines
5.4 KiB
Go
package model
|
|
|
|
import "time"
|
|
|
|
// AccessMode 控制客户端的访问范围。
|
|
// - "open": 不限制,客户端可访问所有可达网段(默认)
|
|
// - "whitelist":白名单模式,仅允许访问 Instance 与 User 合并后的 allow_networks
|
|
// 列表中的网段,其他内网流量在服务端 FORWARD 链被丢弃
|
|
type AccessMode string
|
|
|
|
const (
|
|
AccessOpen AccessMode = "open"
|
|
AccessWhitelist AccessMode = "whitelist"
|
|
)
|
|
|
|
// Instance 一个 OpenVPN 服务端实例。
|
|
// 每个实例使用独立端口与 PKI,运行在自己的 server.conf 下,
|
|
// 由 systemd 单元(或后台进程)托管,本服务通过 management 接口与之通信。
|
|
type Instance struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"` // 唯一名,作为目录名
|
|
Port int `json:"port"` // openvpn 监听端口
|
|
Proto string `json:"proto"` // udp / tcp
|
|
Dev string `json:"dev"` // tun / tap
|
|
Subnet string `json:"subnet"` // 客户端子网,如 10.8.0.0/24
|
|
Cipher string `json:"cipher"` // 加密算法
|
|
AuthDigest string `json:"auth_digest"` // 摘要算法
|
|
PushDNS string `json:"push_dns"` // push "dhcp-option DNS x.x.x.x"
|
|
PushRoutes string `json:"push_routes"` // 多行
|
|
Extra string `json:"extra"` // 用户追加配置
|
|
AccessMode AccessMode `json:"access_mode"` // open | whitelist
|
|
AllowNetworks []string `json:"allow_networks"` // 实例级白名单 CIDR 列表
|
|
Status string `json:"status"` // running/stopped/error
|
|
PID int `json:"pid"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// VPNUser 一个 OpenVPN 客户端用户。
|
|
// 证书从对应实例的 PKI 中签发,可下载 .ovpn 客户端配置。
|
|
type VPNUser struct {
|
|
ID string `json:"id"`
|
|
InstanceID string `json:"instance_id"`
|
|
Username string `json:"username"` // 证书 CN
|
|
RealName string `json:"real_name"` // 备注
|
|
Email string `json:"email"`
|
|
Enabled bool `json:"enabled"` // 是否启用, false 即吊销/禁用
|
|
StaticIP string `json:"static_ip"` // ccd 固定地址, 空表示动态
|
|
AllowNetworks []string `json:"allow_networks"` // 用户级白名单(在实例基础上叠加)
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
|
}
|
|
|
|
// AuditLog 操作审计日志
|
|
type AuditLog struct {
|
|
ID string `json:"id"`
|
|
Time time.Time `json:"time"`
|
|
User string `json:"user"` // 操作者(管理用户)
|
|
Action string `json:"action"` // create_instance, revoke_user, ...
|
|
Target string `json:"target"` // 目标对象
|
|
Result string `json:"result"` // ok / failed
|
|
Detail string `json:"detail"`
|
|
IP string `json:"ip"`
|
|
}
|
|
|
|
// AdminUser 管理控制台账号。
|
|
// 密码以 bcrypt 哈希存储 (cost=10)。
|
|
// - Role: "admin" = 全部权限 (含账号管理)
|
|
// "operator" = 仅运维操作(实例/用户/备份等),无账号管理
|
|
// - Status: "active" | "disabled"
|
|
// - MustChangePassword: 首次 seed 的默认账号(密码 = admin123)需要首次登录后改密
|
|
//
|
|
// 安全:
|
|
// * PasswordHash 字段在 db.json 中以 password_hash 持久化(必须!)
|
|
// * 但 store.ListAdmins 在返回前会清空 PasswordHash,
|
|
// 因此所有 API 响应里 hash 都是空字符串,绝不出网
|
|
type AdminUser struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
PasswordHash string `json:"password_hash"` // 注意:store.ListAdmins 返回时会清空
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
|
LastLoginIP string `json:"last_login_ip,omitempty"`
|
|
MustChangePassword bool `json:"must_change_password"` // 强制改密标志
|
|
}
|
|
|
|
// ConnectionLog 来自 OpenVPN status 的实时/历史连接记录。
|
|
// 周期由 OpenVPN 自身写入 status.log,本服务周期性读取解析后入库。
|
|
type ConnectionLog struct {
|
|
InstanceID string `json:"instance_id"`
|
|
CommonName string `json:"common_name"`
|
|
RealIP string `json:"real_ip"` // 客户端公网 IP
|
|
VPNIP string `json:"vpn_ip"` // 分配的 VPN 内网 IP
|
|
BytesIn int64 `json:"bytes_in"`
|
|
BytesOut int64 `json:"bytes_out"`
|
|
ConnectedAt time.Time `json:"connected_at"`
|
|
DisconnectedAt *time.Time `json:"disconnected_at,omitempty"`
|
|
}
|
|
|
|
// Backup 一份备份归档
|
|
type Backup struct {
|
|
ID string `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Size int64 `json:"size"`
|
|
Note string `json:"note"`
|
|
Filename string `json:"filename"`
|
|
Includes []string `json:"includes"`
|
|
}
|
|
|
|
// CertInfo 证书元数据(用于证书到期提醒)。
|
|
type CertInfo struct {
|
|
InstanceID string `json:"instance_id"`
|
|
Username string `json:"username"`
|
|
Subject string `json:"subject"`
|
|
NotBefore time.Time `json:"not_before"`
|
|
NotAfter time.Time `json:"not_after"`
|
|
DaysLeft int `json:"days_left"`
|
|
} |