create-windy 0.2.6 → 0.2.7

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.
package/README.md CHANGED
@@ -99,9 +99,14 @@ bun run dev
99
99
  ```
100
100
 
101
101
  create-windy 会生成仅供本机使用、被 Git 忽略且权限为 `0600` 的 `.env`,其中数据库
102
- 与管理员开发密码均为随机值。`bun run dev` 会通过 Docker Compose 等待 PostgreSQL、
103
- 执行 migration,再启动 Server 与 Web;首次运行前需安装并启动 Docker 与 Docker
104
- Compose。
102
+ 与管理员开发密码均为随机值。全新数据库的初始平台账号固定为 `admin`,初始密码查看
103
+ `.env` 中的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD`;创建器只提示密码位置,不会把随机密码
104
+ 直接打印在终端。首次登录必须修改密码,新密码需要 12-128 个字符,且不能与当前密码
105
+ 相同。
106
+
107
+ `bun run dev` 会通过 Docker Compose 等待 PostgreSQL、执行 migration,再启动 Server
108
+ 与 Web;首次运行前需安装并启动 Docker 与 Docker Compose。环境变量只负责初始化全新
109
+ 数据库,数据库已有管理员后,重启服务不会重新覆盖其密码。
105
110
 
106
111
  只调试单个应用时可分别使用 `bun run dev:web` 和 `bun run dev:server`。`dev:web` 不会
107
112
  隐式启动后端;`dev:server` 也不会自动启动 PostgreSQL 或执行 migration。
package/dist/cli.js CHANGED
@@ -1443,6 +1443,14 @@ bun run dev:server
1443
1443
 
1444
1444
  ## 页面与登录
1445
1445
 
1446
+ 全新数据库的初始平台账号为 \`admin\`。初始密码不是所有项目共用的固定值,而是
1447
+ create-windy 在本机 \`.env\` 中随机生成的 \`WINDY_BOOTSTRAP_ADMIN_PASSWORD\`;该文件已
1448
+ 被 Git 忽略,请在本机直接查看,不要把密码提交到仓库或复制到文档。首次登录会要求
1449
+ 立即修改密码,新密码必须为 12-128 个字符,且不能与当前密码相同。
1450
+
1451
+ \`.env\` 只负责全新数据库的首次初始化。账号密码写入数据库后,重启不会用环境变量
1452
+ 覆盖已有密码;如果已经完成首次改密,应使用修改后的密码登录。
1453
+
1446
1454
  | 路径 | 访问要求 | 说明 |
1447
1455
  | ---- | -------- | ---- |
1448
1456
  | \`/login\` | 公开 | 登录成功后回到安全的站内目标,默认进入 \`/\` |
@@ -1907,6 +1915,8 @@ async function createProject(options, dependencies = {}) {
1907
1915
  log(`运行时:Bun ${bunVersion}`);
1908
1916
  log(options.initializeGit ? gitInitialized ? "Git:已创建 main 分支和首次提交" : "Git:项目已保留,请按上述提示完成初始化" : "Git:已按 --no-git 跳过");
1909
1917
  log(localEnvironment === "created" ? "本地 .env:已生成随机开发密码(权限 0600)" : "本地 .env:已存在,未覆盖");
1918
+ log("初始平台账号:admin");
1919
+ log("初始平台密码:查看 .env 中的 WINDY_BOOTSTRAP_ADMIN_PASSWORD(首次登录后必须修改)");
1910
1920
  log("下一步(启动数据库、迁移、后端与前端):");
1911
1921
  log(` cd ${options.projectName}`);
1912
1922
  log(" bun run dev");
@@ -3897,7 +3907,8 @@ var recipes = [
3897
3907
  { id: "starter-0.2.2-to-0.2.3", from: "0.2.2", to: "0.2.3" },
3898
3908
  { id: "starter-0.2.3-to-0.2.4", from: "0.2.3", to: "0.2.4" },
3899
3909
  { id: "starter-0.2.4-to-0.2.5", from: "0.2.4", to: "0.2.5" },
3900
- { id: "starter-0.2.5-to-0.2.6", from: "0.2.5", to: "0.2.6" }
3910
+ { id: "starter-0.2.5-to-0.2.6", from: "0.2.5", to: "0.2.6" },
3911
+ { id: "starter-0.2.6-to-0.2.7", from: "0.2.6", to: "0.2.7" }
3901
3912
  ];
3902
3913
  function resolveRecipeChain(sourceVersion, targetVersion) {
3903
3914
  if (sourceVersion === targetVersion)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "8b44208f73dcd5879232b3151c5344abfaeff53e",
4
- "generatorVersion": "0.2.6",
3
+ "templateCommit": "93d68f895e71560c1f85d283dce5d8b94edfd09f",
4
+ "generatorVersion": "0.2.7",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -52,6 +52,14 @@ bun run dev:server
52
52
 
53
53
  ## 页面与登录
54
54
 
55
+ 全新数据库的初始平台账号为 `admin`。初始密码不是所有项目共用的固定值,而是
56
+ create-windy 在本机 `.env` 中随机生成的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD`;该文件已
57
+ 被 Git 忽略,请在本机直接查看,不要把密码提交到仓库或复制到文档。首次登录会要求
58
+ 立即修改密码,新密码必须为 12-128 个字符,且不能与当前密码相同。
59
+
60
+ `.env` 只负责全新数据库的首次初始化。账号密码写入数据库后,重启不会用环境变量
61
+ 覆盖已有密码;如果已经完成首次改密,应使用修改后的密码登录。
62
+
55
63
  | 路径 | 访问要求 | 说明 |
56
64
  | ---- | -------- | ---- |
57
65
  | `/login` | 公开 | 登录成功后回到安全的站内目标,默认进入 `/` |
@@ -1,9 +1,7 @@
1
1
  import argon2, { argon2id } from "argon2";
2
+ import { PASSWORD_POLICY } from "@windy/shared";
2
3
 
3
- export const PASSWORD_POLICY = {
4
- minLength: 12,
5
- maxLength: 128,
6
- } as const;
4
+ export { PASSWORD_POLICY } from "@windy/shared";
7
5
 
8
6
  const ARGON2_OPTIONS = {
9
7
  type: argon2id,
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, shallowRef } from "vue";
3
3
  import { KeyRoundIcon, LoaderCircleIcon } from "@lucide/vue";
4
+ import { PASSWORD_POLICY } from "@windy/shared";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Input } from "@/components/ui/input";
6
7
  import type { PasswordChangeInput } from "@/services/auth-api";
@@ -11,7 +12,20 @@ const emit = defineEmits<{ submit: [input: PasswordChangeInput] }>();
11
12
  const currentPassword = shallowRef("");
12
13
  const newPassword = shallowRef("");
13
14
  const confirmation = shallowRef("");
14
- const localError = computed(() => {
15
+ const newPasswordError = computed(() => {
16
+ if (!newPassword.value) return "";
17
+ if (newPassword.value.length < PASSWORD_POLICY.minLength) {
18
+ return `新密码至少需要 ${PASSWORD_POLICY.minLength} 个字符,当前为 ${newPassword.value.length} 个。`;
19
+ }
20
+ if (newPassword.value.length > PASSWORD_POLICY.maxLength) {
21
+ return `新密码不能超过 ${PASSWORD_POLICY.maxLength} 个字符。`;
22
+ }
23
+ if (currentPassword.value && newPassword.value === currentPassword.value) {
24
+ return "新密码不能与当前密码相同。";
25
+ }
26
+ return "";
27
+ });
28
+ const confirmationError = computed(() => {
15
29
  if (confirmation.value && confirmation.value !== newPassword.value) {
16
30
  return "两次输入的新密码不一致";
17
31
  }
@@ -21,7 +35,9 @@ const canSubmit = computed(
21
35
  () =>
22
36
  !props.loading &&
23
37
  currentPassword.value.length > 0 &&
24
- newPassword.value.length >= 12 &&
38
+ newPassword.value.length > 0 &&
39
+ !newPasswordError.value &&
40
+ !confirmationError.value &&
25
41
  newPassword.value === confirmation.value,
26
42
  );
27
43
 
@@ -54,12 +70,31 @@ function submit() {
54
70
  v-model="newPassword"
55
71
  type="password"
56
72
  autocomplete="new-password"
57
- minlength="12"
58
- maxlength="128"
73
+ :minlength="PASSWORD_POLICY.minLength"
74
+ :maxlength="PASSWORD_POLICY.maxLength"
75
+ :aria-describedby="
76
+ newPasswordError
77
+ ? 'new-password-help new-password-error'
78
+ : 'new-password-help'
79
+ "
80
+ :aria-invalid="Boolean(newPasswordError)"
59
81
  :disabled="loading"
60
82
  required
61
83
  />
62
- <p class="text-xs text-muted-foreground">请输入 12-128 个字符。</p>
84
+ <p id="new-password-help" class="text-xs text-muted-foreground">
85
+ 密码要求:{{ PASSWORD_POLICY.minLength }}-{{
86
+ PASSWORD_POLICY.maxLength
87
+ }}
88
+ 个字符,且不能与当前密码相同。
89
+ </p>
90
+ <p
91
+ v-if="newPasswordError"
92
+ id="new-password-error"
93
+ class="text-sm text-destructive"
94
+ role="alert"
95
+ >
96
+ {{ newPasswordError }}
97
+ </p>
63
98
  </div>
64
99
  <div class="space-y-2">
65
100
  <label for="confirm-password" class="text-sm font-medium"
@@ -70,12 +105,22 @@ function submit() {
70
105
  v-model="confirmation"
71
106
  type="password"
72
107
  autocomplete="new-password"
108
+ :aria-describedby="confirmationError ? 'confirmation-error' : undefined"
109
+ :aria-invalid="Boolean(confirmationError)"
73
110
  :disabled="loading"
74
111
  required
75
112
  />
113
+ <p
114
+ v-if="confirmationError"
115
+ id="confirmation-error"
116
+ class="text-sm text-destructive"
117
+ role="alert"
118
+ >
119
+ {{ confirmationError }}
120
+ </p>
76
121
  </div>
77
- <p v-if="localError || error" class="text-sm text-destructive" role="alert">
78
- {{ localError || error }}
122
+ <p v-if="error" class="text-sm text-destructive" role="alert">
123
+ {{ error }}
79
124
  </p>
80
125
  <Button type="submit" class="w-full" :disabled="!canSubmit">
81
126
  <LoaderCircleIcon
@@ -0,0 +1,43 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import PasswordChangeForm from "./PasswordChangeForm.vue";
4
+
5
+ describe("首次改密表单", () => {
6
+ test("输入过短密码时立即说明原因并保持提交禁用", async () => {
7
+ const wrapper = mount(PasswordChangeForm);
8
+
9
+ await wrapper.get("#current-password").setValue("initial-password");
10
+ await wrapper.get("#new-password").setValue("weak");
11
+ await wrapper.get("#confirm-password").setValue("weak");
12
+
13
+ expect(wrapper.get("[role='alert']").text()).toBe(
14
+ "新密码至少需要 12 个字符,当前为 4 个。",
15
+ );
16
+ expect(wrapper.get("button[type='submit']").attributes()).toHaveProperty(
17
+ "disabled",
18
+ );
19
+ expect(wrapper.emitted("submit")).toBeUndefined();
20
+ });
21
+
22
+ test("满足规则且两次一致后允许提交", async () => {
23
+ const wrapper = mount(PasswordChangeForm);
24
+
25
+ await wrapper.get("#current-password").setValue("initial-password");
26
+ await wrapper.get("#new-password").setValue("new-password-2026");
27
+ await wrapper.get("#confirm-password").setValue("new-password-2026");
28
+
29
+ expect(wrapper.find("[role='alert']").exists()).toBe(false);
30
+ expect(
31
+ wrapper.get("button[type='submit']").attributes("disabled"),
32
+ ).toBeUndefined();
33
+ await wrapper.get("form").trigger("submit");
34
+ expect(wrapper.emitted("submit")).toEqual([
35
+ [
36
+ {
37
+ currentPassword: "initial-password",
38
+ newPassword: "new-password-2026",
39
+ },
40
+ ],
41
+ ]);
42
+ });
43
+ });
@@ -6,6 +6,7 @@ export * from "./src/license.js";
6
6
  export * from "./src/module-configuration.js";
7
7
  export * from "./src/correlation.js";
8
8
  export * from "./src/notification.js";
9
+ export * from "./src/password.js";
9
10
  export * from "./src/platform-settings.js";
10
11
  export * from "./src/profile.js";
11
12
  export * from "./src/primitives.js";
@@ -0,0 +1,4 @@
1
+ export const PASSWORD_POLICY = {
2
+ minLength: 12,
3
+ maxLength: 128,
4
+ } as const;