diff --git a/.gitignore b/.gitignore index 6e12987..6e9f785 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,8 @@ Thumbs.db /server/webui/dist/ /dist/ +# JWT 签名密钥(运行时生成,存项目根,绝不提交) +/jwt.key + # Docker 部署生成的本地环境(含密码/密钥,不提交) .env \ No newline at end of file diff --git a/cmd/artex/main.go b/cmd/artex/main.go index 2bda91b..a2b6611 100644 --- a/cmd/artex/main.go +++ b/cmd/artex/main.go @@ -80,7 +80,7 @@ func main() { skillDir = abs } log.Printf("[config] skill 目录: %s", skillDir) - srv := server.New(ctx, mgr, skillDir, *dataDir) + srv := server.New(ctx, mgr, skillDir, *dataDir, config.BaseDir()) httpSrv := &http.Server{ Addr: *addr, Handler: srv.Handler(), diff --git a/server/auth.go b/server/auth.go index 26aee9e..2fa398b 100644 --- a/server/auth.go +++ b/server/auth.go @@ -22,10 +22,26 @@ const ( keyChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) -// loadOrCreateJWTKey reads the 32-byte signing key from dataDir/jwt.key. -// On first run it generates a cryptographically random key and persists it. -func loadOrCreateJWTKey(dataDir string) ([]byte, error) { - path := filepath.Join(dataDir, jwtKeyFilename) +// loadOrCreateJWTKey reads the 32-byte signing key from keyDir/jwt.key. keyDir is +// the project base dir (next to the executable), NOT the browsable workspace root +// (dataDir) — the signing key must never be listable/downloadable via the file +// manager. Legacy installs kept it at dataDir/jwt.key; if present there and not yet +// at the new location, it is migrated (key preserved, so sessions stay valid) and +// the old file removed so it disappears from the workspace. On first run a random +// key is generated and persisted. +func loadOrCreateJWTKey(keyDir, dataDir string) ([]byte, error) { + path := filepath.Join(keyDir, jwtKeyFilename) + // one-time migration out of the old in-workspace location. + if legacy := filepath.Join(dataDir, jwtKeyFilename); legacy != path { + if _, err := os.Stat(path); os.IsNotExist(err) { + if data, rerr := os.ReadFile(legacy); rerr == nil { + if werr := os.WriteFile(path, data, 0o600); werr == nil { + _ = os.Remove(legacy) + log.Printf("[auth] JWT key 已从 %s 迁移到 %s(移出可浏览工作区)", legacy, path) + } + } + } + } if data, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(data))) >= 32 { return []byte(strings.TrimSpace(string(data))), nil } diff --git a/server/core_test.go b/server/core_test.go index ae9c8ca..49e9417 100644 --- a/server/core_test.go +++ b/server/core_test.go @@ -22,7 +22,7 @@ func TestCoreTaskLifecyclePG(t *testing.T) { } defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td) + s := New(context.Background(), m, td, td, td) h := s.Handler() tok, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/mgmt_test.go b/server/mgmt_test.go index cb2c54d..9bfebb7 100644 --- a/server/mgmt_test.go +++ b/server/mgmt_test.go @@ -19,7 +19,7 @@ func TestMgmtAPI(t *testing.T) { t.Skip("postgres unavailable — skipping management API test") } td := t.TempDir() - s := New(context.Background(), m, td, td) + s := New(context.Background(), m, td, td, td) h := s.Handler() tok, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/server.go b/server/server.go index 0729c8d..0ec0271 100644 --- a/server/server.go +++ b/server/server.go @@ -97,8 +97,8 @@ type triggeredRun struct { mergeable bool // true for finding/goal event triggers (merge by taskID) } -func New(ctx context.Context, m *Manager, skillDir string, dataDir string) *Server { - key, err := loadOrCreateJWTKey(dataDir) +func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDir string) *Server { + key, err := loadOrCreateJWTKey(keyDir, dataDir) if err != nil { log.Fatalf("[auth] JWT key: %v", err) }