新增 reset-password 子命令:免旧口令重设管理员口令并吊销全部会话
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
- **博客**:首页(置顶优先 + 分页 + 标签云)、详情(上一篇/下一篇)、标签页、RSS、sitemap、SEO meta 注入(仅公开内容)
|
- **博客**:首页(置顶优先 + 分页 + 标签云)、详情(上一篇/下一篇)、标签页、RSS、sitemap、SEO meta 注入(仅公开内容)
|
||||||
- **管理**:单管理员口令登录(Argon2id)、会话 7 天滑动续期(CSRF 轮换保持不变)、防爆破限流(per-IP + per-账号)、改密
|
- **管理**:单管理员口令登录(Argon2id)、会话 7 天滑动续期(CSRF 轮换保持不变)、防爆破限流(per-IP + per-账号)、改密
|
||||||
- **回收站**:删除 = 软删除,30 天内可恢复,`gc` 到期物理清除
|
- **回收站**:删除 = 软删除,30 天内可恢复,`gc` 到期物理清除
|
||||||
- **运维**:`init` / `serve` / `backup`(在线一致快照)/ `gc`(默认 dry-run)/ `version`
|
- **运维**:`init` / `reset-password`(重设口令并吊销全部会话)/ `serve` / `backup`(在线一致快照)/ `gc`(默认 dry-run)/ `version`
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -39,6 +39,13 @@ make build
|
|||||||
cd web && npm install && npm run dev # /api 代理到 127.0.0.1:8080
|
cd web && npm install && npm run dev # /api 代理到 127.0.0.1:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
忘记口令(CLI 可达即具备服务器权限,可直接重设;重设后全部会话被吊销):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./pure-note reset-password --data-dir ./data
|
||||||
|
# 非交互:PN_ADMIN_PASSWORD=xxx ./pure-note reset-password --data-dir ./data
|
||||||
|
```
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -71,7 +78,7 @@ sudo systemctl enable --now pure-note pure-note-maint.timer
|
|||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
cmd/pure-note/ CLI(serve/init/backup/gc/version)
|
cmd/pure-note/ CLI(serve/init/reset-password/backup/gc/version)
|
||||||
internal/config/ 命令行解析(--dev loopback 守卫)
|
internal/config/ 命令行解析(--dev loopback 守卫)
|
||||||
internal/store/ SQLite(modernc 纯 Go 驱动):迁移(user_version) + DAO + 备份 + gc
|
internal/store/ SQLite(modernc 纯 Go 驱动):迁移(user_version) + DAO + 备份 + gc
|
||||||
internal/auth/ Argon2id(PHC) + 随机 token
|
internal/auth/ Argon2id(PHC) + 随机 token
|
||||||
|
|||||||
+46
-1
@@ -1,4 +1,4 @@
|
|||||||
// pure-note 单二进制入口:serve / init / backup / gc / version(§7.6)。
|
// pure-note 单二进制入口:serve / init / reset-password / backup / gc / version(§7.6)。
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -42,6 +42,8 @@ func main() {
|
|||||||
err = runServe(os.Args[2:])
|
err = runServe(os.Args[2:])
|
||||||
case "init":
|
case "init":
|
||||||
err = runInit(os.Args[2:])
|
err = runInit(os.Args[2:])
|
||||||
|
case "reset-password":
|
||||||
|
err = runResetPassword(os.Args[2:])
|
||||||
case "backup":
|
case "backup":
|
||||||
err = runBackup(os.Args[2:])
|
err = runBackup(os.Args[2:])
|
||||||
case "gc":
|
case "gc":
|
||||||
@@ -67,6 +69,7 @@ func usage() {
|
|||||||
用法:
|
用法:
|
||||||
pure-note serve [--addr :8080] [--data-dir ./data] [--behind-proxy] [--dev] [--allow-newer]
|
pure-note serve [--addr :8080] [--data-dir ./data] [--behind-proxy] [--dev] [--allow-newer]
|
||||||
pure-note init [--data-dir ./data]
|
pure-note init [--data-dir ./data]
|
||||||
|
pure-note reset-password [--data-dir ./data] # 重设管理员口令并吊销全部会话
|
||||||
pure-note backup [--data-dir ./data] [输出路径]
|
pure-note backup [--data-dir ./data] [输出路径]
|
||||||
pure-note gc [--data-dir ./data] [--commit] # 默认 dry-run
|
pure-note gc [--data-dir ./data] [--commit] # 默认 dry-run
|
||||||
pure-note version
|
pure-note version
|
||||||
@@ -225,6 +228,48 @@ func runInit(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runResetPassword 重设管理员口令:CLI 可达即具备服务器权限,允许直接覆盖。
|
||||||
|
// 口令来源与 init 一致:环境变量 PN_ADMIN_PASSWORD 或交互输入(避免出现在 ps 参数里)。
|
||||||
|
// 重设成功后吊销全部会话,强制所有端重新登录。
|
||||||
|
func runResetPassword(args []string) error {
|
||||||
|
cfg, _, err := config.ParseMaint("reset-password", args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
st, err := store.Open(cfg.DBPath(), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
var password string
|
||||||
|
if env := os.Getenv("PN_ADMIN_PASSWORD"); env != "" {
|
||||||
|
password = env
|
||||||
|
fmt.Fprintln(os.Stderr, "已从环境变量 PN_ADMIN_PASSWORD 读取口令")
|
||||||
|
} else {
|
||||||
|
password, err = promptPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := auth.CheckPasswordStrength(password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash, err := auth.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := st.SetSetting(store.KeyAdminPasswordHash, hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
revoked, err := st.DeleteAllSessions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("口令已重设;已吊销 %d 个会话,所有端需重新登录。\n", revoked)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func promptPassword() (string, error) {
|
func promptPassword() (string, error) {
|
||||||
fmt.Print("设置管理员口令(≥12 字符): ")
|
fmt.Print("设置管理员口令(≥12 字符): ")
|
||||||
b1, err := term.ReadPassword(int(os.Stdin.Fd()))
|
b1, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
pure-note(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
pure-note(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
||||||
├── cmd/pure-note/ serve / init / backup / gc / version
|
├── cmd/pure-note/ serve / init / reset-password / backup / gc / version
|
||||||
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
|
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
|
||||||
├── web/ React 19 + Vite 8 + Tailwind 4(vitest 6 用例)
|
├── web/ React 19 + Vite 8 + Tailwind 4(vitest 6 用例)
|
||||||
├── deploy/ pure-note.service、pure-note-maint.{service,timer}、Caddyfile
|
├── deploy/ pure-note.service、pure-note-maint.{service,timer}、Caddyfile
|
||||||
|
|||||||
+4
-2
@@ -177,7 +177,7 @@ pure-note/
|
|||||||
│ └── review-round1.md # 第 1 轮审评报告(修订依据,留档)
|
│ └── review-round1.md # 第 1 轮审评报告(修订依据,留档)
|
||||||
├── cmd/
|
├── cmd/
|
||||||
│ └── pure-note/
|
│ └── pure-note/
|
||||||
│ └── main.go # 入口 + 命令行(serve/init/backup/gc/version)
|
│ └── main.go # 入口 + 命令行(serve/init/reset-password/backup/gc/version)
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── config/ # 环境变量/flag 解析
|
│ ├── config/ # 环境变量/flag 解析
|
||||||
│ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO
|
│ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO
|
||||||
@@ -355,7 +355,8 @@ SecurityHeaders(含 HSTS,§9.2)
|
|||||||
5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感);
|
5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感);
|
||||||
6. **登出**:删除会话行;
|
6. **登出**:删除会话行;
|
||||||
7. **改密**:`POST /api/admin/password` 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制);
|
7. **改密**:`POST /api/admin/password` 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制);
|
||||||
8. **开发模式**:`--dev` 允许非 Secure Cookie(否则 `__Host-` 在 `http://局域网IP` 下无法登录);**强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产**。
|
8. **口令重置(CLI)**:`pure-note reset-password` 免旧口令直接覆盖哈希并**吊销全部会话**——前提是具备服务器访问权限(读写数据目录即可改库,CLI 提供规范入口优于手工改库);口令来源与 `init` 一致,避免出现在 argv;
|
||||||
|
9. **开发模式**:`--dev` 允许非 Secure Cookie(否则 `__Host-` 在 `http://局域网IP` 下无法登录);**强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产**。
|
||||||
|
|
||||||
### 7.4 图片上传与访问
|
### 7.4 图片上传与访问
|
||||||
|
|
||||||
@@ -377,6 +378,7 @@ SecurityHeaders(含 HSTS,§9.2)
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `pure-note serve` | 启动;`--addr :8080`、`--data-dir ./data`、`--log-level`、`--log-format text|json`、`--behind-proxy`(声明位于可信反代之后,启用 XFF 处理)、`--dev`(loopback-only,§7.3-8)、`--allow-newer`(跳过 schema 版本上界校验,§10.4);环境变量:`PN_ADMIN_PASSWORD` |
|
| `pure-note serve` | 启动;`--addr :8080`、`--data-dir ./data`、`--log-level`、`--log-format text|json`、`--behind-proxy`(声明位于可信反代之后,启用 XFF 处理)、`--dev`(loopback-only,§7.3-8)、`--allow-newer`(跳过 schema 版本上界校验,§10.4);环境变量:`PN_ADMIN_PASSWORD` |
|
||||||
| `pure-note init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
| `pure-note init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
||||||
|
| `pure-note reset-password` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 |
|
||||||
| `pure-note backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
| `pure-note backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
||||||
| `pure-note gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
| `pure-note gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
||||||
| `pure-note version` | 版本号(build 时注入 commit/时间) |
|
| `pure-note version` | 版本号(build 时注入 commit/时间) |
|
||||||
|
|||||||
@@ -76,3 +76,12 @@ func (s *Store) DeleteExpiredSessions(now int64) (int64, error) {
|
|||||||
}
|
}
|
||||||
return res.RowsAffected()
|
return res.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteAllSessions 吊销全部会话(口令重置后强制所有端重新登录)。
|
||||||
|
func (s *Store) DeleteAllSessions() (int64, error) {
|
||||||
|
res, err := s.db.Exec(`DELETE FROM sessions`)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user