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 +8 -3
- package/dist/cli.js +12 -1
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +8 -0
- package/template/apps/server/src/auth/password.ts +2 -4
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +52 -7
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +43 -0
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/src/password.ts +4 -0
package/README.md
CHANGED
|
@@ -99,9 +99,14 @@ bun run dev
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
create-windy 会生成仅供本机使用、被 Git 忽略且权限为 `0600` 的 `.env`,其中数据库
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
package/template/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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="
|
|
58
|
-
maxlength="
|
|
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"
|
|
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="
|
|
78
|
-
{{
|
|
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";
|