@gadmin2n/schematics 0.0.118 → 0.0.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ # Age + SOPS 使用指引(.env)
2
+
3
+ ## 目标
4
+
5
+ 使用 Age + SOPS 对 `.env` 加密,Git 中仅保存 `.env.enc`。
6
+
7
+ ## 安装
8
+
9
+ ``` bash
10
+ brew install age sops
11
+ ```
12
+
13
+ | 平台 | age | SOPS |
14
+ | --------------- | ---------------------- | ------------------------ |
15
+ | macOS | `brew install age` | `brew install sops` |
16
+ | Ubuntu / Debian | `sudo apt install age` | 下载官方 Release(二进制) |
17
+ | Rocky / CentOS | `sudo dnf install age` | 下载官方 Release(二进制) |
18
+ | Alpine | `apk add age` | `apk add sops` |
19
+ | Docker | `apt install age` | 下载官方 Release(二进制) |
20
+ | GitHub Actions | `apt install age` | `getsops/sops-installer` |
21
+
22
+
23
+ ## 生成密钥(每人独立)
24
+
25
+ 每位团队成员在自己的机器上生成专属密钥:
26
+
27
+ ``` bash
28
+ age-keygen -o key.txt
29
+ mkdir -p ~/.config/sops/age
30
+ mv key.txt ~/.config/sops/age/keys.txt
31
+ ```
32
+
33
+ > 私钥只存在于本机,**绝不共享、绝不上传**。
34
+
35
+ 将自己的**公钥**(`# public key:` 那一行)告知团队( 就是放入`.sops.yaml`,放在项目根目录)
36
+
37
+ ## 创建 `.sops.yaml`(多收件人)
38
+
39
+ 将所有团队成员的公钥列入,SOPS 加密时会同时为每个人加密,每人用自己的私钥均可解密:
40
+
41
+ ``` yaml
42
+ creation_rules:
43
+ - path_regex: .*\.env$
44
+ age: >-
45
+ age1alice_PUBLIC_KEY,
46
+ age1bob_PUBLIC_KEY,
47
+ age1charlie_PUBLIC_KEY
48
+ ```
49
+
50
+ > `.sops.yaml` **提交进 Git**,公钥可以公开。
51
+
52
+ ### 人员变更
53
+
54
+ **新人加入:** 将其公钥追加到 `.sops.yaml`,然后执行:
55
+ ``` bash
56
+ sops updatekeys .env.enc
57
+ ```
58
+
59
+ **人员离职:** 从 `.sops.yaml` 删除其公钥,同样执行 `sops updatekeys .env.enc`。其旧私钥立即失去解密能力,无需重新分发任何密钥。
60
+
61
+ ## 首次加密
62
+
63
+ ``` bash
64
+ vi .env
65
+ sops encrypt .env > .env.enc
66
+ ```
67
+
68
+ ## 修改
69
+
70
+ ``` bash
71
+ sops .env.enc
72
+ ```
73
+ > 会自动用vi/vim打开,编辑完自动加密,不需要手工:`decrypt -> edit -> encrypt`
74
+
75
+ ## 本地运行
76
+
77
+ ### 方法一:生成临时 .env
78
+
79
+ ``` bash
80
+ sops -d .env.enc > .env
81
+ node server.js
82
+ ```
83
+
84
+ > 需要严格禁止Coding agent访问 `.env` 文件
85
+
86
+ ### 方法二:导入环境变量
87
+
88
+ ``` bash
89
+ set -a
90
+ source <(sops -d .env.enc)
91
+ set +a
92
+ node server.js
93
+ ```
94
+
95
+ 应用继续使用:
96
+
97
+ ``` js
98
+ process.env.XXX
99
+ ```
100
+
101
+ ## 准备部署专用密钥
102
+
103
+ 部署环境使用独立的 deploy key,**不使用任何个人私钥**,避免人员变更影响部署。
104
+
105
+ ``` bash
106
+ # 生成 deploy key(在本地安全环境执行)
107
+ age-keygen -o deploy-key.txt
108
+ ```
109
+
110
+ 将 deploy key 的公钥加入 `.sops.yaml`,然后重新加密:
111
+
112
+ ``` bash
113
+ sops updatekeys .env.enc
114
+ ```
115
+
116
+ > `deploy-key.txt` 私钥只上传到部署平台的 Secrets,不提交 Git,本地用完可删除。
117
+
118
+ ## 部署(VM / Linux)
119
+
120
+ 将 deploy 私钥放置:
121
+
122
+ ``` text
123
+ /etc/sops/key.txt
124
+ ```
125
+
126
+ 设置权限:
127
+
128
+ ``` bash
129
+ chmod 600 /etc/sops/key.txt
130
+ export SOPS_AGE_KEY_FILE=/etc/sops/key.txt
131
+ ```
132
+
133
+ 启动:
134
+
135
+ ``` bash
136
+ sops -d .env.enc > .env
137
+ exec node server.js
138
+ ```
139
+
140
+ ## Kubernetes 部署
141
+
142
+ ### 创建 Secret
143
+
144
+ ``` bash
145
+ kubectl create secret generic sops-age-key \
146
+ --from-file=key.txt=deploy-key.txt
147
+ ```
148
+
149
+ ### Deployment
150
+
151
+ 将 Secret 以 volume 挂载,通过 entrypoint 脚本启动:
152
+
153
+ ``` sh
154
+ #!/bin/sh
155
+ set -e
156
+
157
+ export SOPS_AGE_KEY_FILE=/etc/sops/key.txt
158
+ sops -d /app/.env.enc > /app/.env
159
+
160
+ exec node server.js
161
+ ```
162
+
163
+ Dockerfile:
164
+
165
+ ``` dockerfile
166
+ ENTRYPOINT ["/app/entrypoint.sh"]
167
+ ```
168
+
169
+ 相关 Deployment yaml 片段:
170
+
171
+ ``` yaml
172
+ volumes:
173
+ - name: sops-age-key
174
+ secret:
175
+ secretName: sops-age-key
176
+ containers:
177
+ - name: app
178
+ volumeMounts:
179
+ - name: sops-age-key
180
+ mountPath: /etc/sops
181
+ readOnly: true
182
+ ```
183
+
184
+ ## GitHub Actions 部署
185
+
186
+ 将 deploy 私钥内容存入 GitHub Secrets(如 `SOPS_AGE_KEY`),在 workflow 中使用:
187
+
188
+ ``` yaml
189
+ - name: Decrypt .env
190
+ run: |
191
+ echo "${{ secrets.SOPS_AGE_KEY }}" > /tmp/age-key.txt
192
+ export SOPS_AGE_KEY_FILE=/tmp/age-key.txt
193
+ sops -d .env.enc > .env
194
+ ```
195
+
196
+ ## Git 建议
197
+
198
+ 提交:
199
+
200
+ - `.env.enc`
201
+ - `.sops.yaml`
202
+
203
+ 不要提交:
204
+
205
+ - `.env`
206
+ - `keys.txt`
207
+
208
+ `.gitignore`
209
+
210
+ ``` gitignore
211
+ .env
212
+ ```
213
+
214
+ ## 最佳实践
215
+
216
+ - 一个环境一个 `.env.enc`
217
+ - 私钥绝不进入 Git
218
+ - 业务代码始终通过 `process.env` 读取配置
219
+ - 部署时解密,运行结束后如无需要可删除 `.env`
@@ -16,6 +16,7 @@ yarn-debug.log*
16
16
  yarn-error.log*
17
17
  lerna-debug.log*
18
18
  .env.local
19
+ keys.txt
19
20
 
20
21
  # OS
21
22
  .DS_Store
@@ -47,7 +48,8 @@ settings.local.json
47
48
 
48
49
  .agent/
49
50
  .claude-internal/
50
- .claude
51
+ .claude/
52
+ .codebuddy/
51
53
  docs/
52
54
  .frontend-slides/
53
55
  .agent
@@ -10,7 +10,7 @@
10
10
  "@dnd-kit/modifiers": "^9.0.0",
11
11
  "@dnd-kit/sortable": "^7.0.2",
12
12
  "@dnd-kit/utilities": "^3.2.2",
13
- "@gadmin2n/charts": "^0.0.8",
13
+ "@gadmin2n/charts": "0.0.10",
14
14
  "@gadmin2n/react-common": "^0.0.70",
15
15
  "@monaco-editor/react": "^4.7.0",
16
16
  "@refinedev/antd": "^5.47.0",
@@ -82,23 +82,27 @@ function writeCache(key: string, value: unknown): void {
82
82
  }
83
83
  }
84
84
 
85
+ // In dev mode, skip sessionStorage cache so menu changes after seed are
86
+ // visible immediately without clearing browser storage manually.
87
+ const IS_DEV = import.meta.env.DEV;
88
+
85
89
  export const useUserPageAccess = (): UseUserPageAccessResult => {
86
90
  const [isIdentityLoading, setIsIdentityLoading] = useState(true);
87
91
 
88
92
  // Role names — initialise from cache so first render is non-empty
89
- const [roleNames, setRoleNames] = useState<string[]>(
90
- () => readCache<string[]>(RoleCacheKey) ?? [],
93
+ const [roleNames, setRoleNames] = useState<string[]>(() =>
94
+ IS_DEV ? [] : (readCache<string[]>(RoleCacheKey) ?? []),
91
95
  );
92
96
 
93
97
  // Data states — initialise from cache for instant first render
94
98
  const [rolesData, setRolesData] = useState<Role[] | null>(() =>
95
- readCache<Role[]>(RolesDataCacheKey),
99
+ IS_DEV ? null : readCache<Role[]>(RolesDataCacheKey),
96
100
  );
97
101
  const [rolePagesData, setRolePagesData] = useState<RolePage[] | null>(() =>
98
- readCache<RolePage[]>(RolePagesCacheKey),
102
+ IS_DEV ? null : readCache<RolePage[]>(RolePagesCacheKey),
99
103
  );
100
104
  const [pagesData, setPagesData] = useState<Page[] | null>(() =>
101
- readCache<Page[]>(PagesCacheKey),
105
+ IS_DEV ? null : readCache<Page[]>(PagesCacheKey),
102
106
  );
103
107
 
104
108
  // Track previous roles to detect changes
@@ -312,7 +316,8 @@ export const useUserPageAccess = (): UseUserPageAccessResult => {
312
316
  // Calculate loading state
313
317
  // If we have cached data, isLoading is false even while background-refreshing
314
318
  const hasCache = useMemo(
315
- () => !!(readCache(PagesCacheKey) && readCache(RolePagesCacheKey)),
319
+ () =>
320
+ !IS_DEV && !!(readCache(PagesCacheKey) && readCache(RolePagesCacheKey)),
316
321
  // eslint-disable-next-line react-hooks/exhaustive-deps
317
322
  [],
318
323
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.118",
3
+ "version": "0.0.120",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [