create-windy 0.2.5 → 0.2.6
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 -0
- package/dist/cli.js +168 -105
- package/package.json +1 -1
- package/template/.dockerignore +1 -0
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +1 -0
- package/template/README.md +15 -7
- package/template/apps/server/src/persistence.ts +2 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +3 -0
- package/template/apps/web/src/pages/ServiceUnavailablePage.vue +38 -0
- package/template/apps/web/src/pages/system/resource-config.ts +2 -0
- package/template/apps/web/src/router/auth-guard.webtest.ts +146 -0
- package/template/apps/web/src/router/auth-navigation.webtest.ts +14 -0
- package/template/apps/web/src/router/index.ts +25 -11
- package/template/apps/web/src/router/index.webtest.ts +26 -209
- package/template/apps/web/src/router/manifest-routes.webtest.ts +49 -0
- package/template/apps/web/src/router/router-test-fixtures.ts +75 -0
- package/template/package.json +1 -1
- package/template/packages/database/src/schema/governance.ts +27 -22
- package/template/packages/database/src/schema/platform-settings.ts +31 -17
- package/template/packages/modules/src/system-admin-routes.ts +2 -0
|
@@ -1,56 +1,19 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import { createMemoryHistory } from "vue-router";
|
|
3
|
-
import { systemFoundationModule } from "@windy/modules";
|
|
4
|
-
import type { WebAccessSnapshot } from "@/layout/navigation";
|
|
5
|
-
import type { PlatformLicenseSnapshot } from "@/services/license-api";
|
|
6
3
|
import { ApiError } from "@/services/http";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
expect(
|
|
19
|
-
routes.find((route) => route.path === "/admin")?.meta?.public,
|
|
20
|
-
).not.toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("每个 Admin Route 都由真实页面实现且 Guard 来自 Manifest", () => {
|
|
24
|
-
const manifests = [systemFoundationModule()];
|
|
25
|
-
const definitions = manifests.flatMap((manifest) => manifest.adminRoutes);
|
|
26
|
-
const implementationByKey = new Map(
|
|
27
|
-
routes.flatMap((route) => {
|
|
28
|
-
const key = route.meta?.manifestRouteKey;
|
|
29
|
-
return typeof key === "string" ? [[key, route] as const] : [];
|
|
30
|
-
}),
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
expect([...implementationByKey.keys()].sort()).toEqual(
|
|
34
|
-
definitions.map(({ key }) => key).sort(),
|
|
35
|
-
);
|
|
36
|
-
for (const definition of definitions) {
|
|
37
|
-
const route = implementationByKey.get(definition.key);
|
|
38
|
-
expect(route?.path).toBe(definition.path);
|
|
39
|
-
expect(route?.meta?.title).toBe(definition.title);
|
|
40
|
-
if (definition.directAccess !== "authenticated") {
|
|
41
|
-
expect(route?.meta?.permissionKey).toBe(definition.permissionKey);
|
|
42
|
-
expect(route?.meta?.featureKey).toBe(definition.featureKey);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
expect(definitions.some(({ path }) => path === "/admin/profile")).toBe(
|
|
46
|
-
true,
|
|
47
|
-
);
|
|
48
|
-
expect(definitions.some(({ path }) => path === "/work-orders")).toBe(false);
|
|
49
|
-
expect(routes.some(({ path }) => path === "/work-orders")).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
4
|
+
import { createAppRouter } from "./index";
|
|
5
|
+
import {
|
|
6
|
+
accessSnapshot,
|
|
7
|
+
authProvider,
|
|
8
|
+
// @windy-module system.license begin
|
|
9
|
+
licenseProvider,
|
|
10
|
+
licenseSnapshot,
|
|
11
|
+
// @windy-module system.license end
|
|
12
|
+
provider,
|
|
13
|
+
} from "./router-test-fixtures";
|
|
52
14
|
|
|
53
15
|
describe("Web 路由强制访问", () => {
|
|
16
|
+
// @windy-module system.license begin
|
|
54
17
|
test("普通业务账号可进入首页且不会加载管理面或 License 快照", async () => {
|
|
55
18
|
let accessRefreshes = 0;
|
|
56
19
|
let licenseRefreshes = 0;
|
|
@@ -78,12 +41,15 @@ describe("Web 路由强制访问", () => {
|
|
|
78
41
|
expect(accessRefreshes).toBe(0);
|
|
79
42
|
expect(licenseRefreshes).toBe(0);
|
|
80
43
|
});
|
|
44
|
+
// @windy-module system.license end
|
|
81
45
|
|
|
82
46
|
test("拥有管理面准入权限的管理员可进入管理后台", async () => {
|
|
83
47
|
const router = createAppRouter(
|
|
84
48
|
createMemoryHistory(),
|
|
85
49
|
provider(accessSnapshot()),
|
|
50
|
+
// @windy-module system.license begin
|
|
86
51
|
licenseProvider(),
|
|
52
|
+
// @windy-module system.license end
|
|
87
53
|
authProvider(),
|
|
88
54
|
);
|
|
89
55
|
|
|
@@ -100,7 +66,9 @@ describe("Web 路由强制访问", () => {
|
|
|
100
66
|
refresh: async () =>
|
|
101
67
|
Promise.reject(new ApiError("admin access denied", 403)),
|
|
102
68
|
},
|
|
69
|
+
// @windy-module system.license begin
|
|
103
70
|
licenseProvider(),
|
|
71
|
+
// @windy-module system.license end
|
|
104
72
|
authProvider(),
|
|
105
73
|
);
|
|
106
74
|
|
|
@@ -119,7 +87,9 @@ describe("Web 路由强制访问", () => {
|
|
|
119
87
|
const router = createAppRouter(
|
|
120
88
|
createMemoryHistory(),
|
|
121
89
|
provider(snapshot),
|
|
90
|
+
// @windy-module system.license begin
|
|
122
91
|
licenseProvider(),
|
|
92
|
+
// @windy-module system.license end
|
|
123
93
|
authProvider(),
|
|
124
94
|
);
|
|
125
95
|
|
|
@@ -138,7 +108,9 @@ describe("Web 路由强制访问", () => {
|
|
|
138
108
|
const router = createAppRouter(
|
|
139
109
|
createMemoryHistory(),
|
|
140
110
|
provider(snapshot),
|
|
111
|
+
// @windy-module system.license begin
|
|
141
112
|
licenseProvider(),
|
|
113
|
+
// @windy-module system.license end
|
|
142
114
|
authProvider(),
|
|
143
115
|
);
|
|
144
116
|
snapshot.features[0]!.allowed = false;
|
|
@@ -161,7 +133,9 @@ describe("Web 路由强制访问", () => {
|
|
|
161
133
|
const router = createAppRouter(
|
|
162
134
|
createMemoryHistory(),
|
|
163
135
|
provider(snapshot),
|
|
136
|
+
// @windy-module system.license begin
|
|
164
137
|
licenseProvider(),
|
|
138
|
+
// @windy-module system.license end
|
|
165
139
|
authProvider(),
|
|
166
140
|
);
|
|
167
141
|
|
|
@@ -183,7 +157,9 @@ describe("Web 路由强制访问", () => {
|
|
|
183
157
|
const router = createAppRouter(
|
|
184
158
|
createMemoryHistory(),
|
|
185
159
|
provider(snapshot),
|
|
160
|
+
// @windy-module system.license begin
|
|
186
161
|
licenseProvider(),
|
|
162
|
+
// @windy-module system.license end
|
|
187
163
|
authProvider(),
|
|
188
164
|
);
|
|
189
165
|
|
|
@@ -192,6 +168,7 @@ describe("Web 路由强制访问", () => {
|
|
|
192
168
|
expect(router.currentRoute.value.path).toBe("/admin/profile");
|
|
193
169
|
});
|
|
194
170
|
|
|
171
|
+
// @windy-module system.license begin
|
|
195
172
|
test("到期后任意业务页面都进入 License 更新页且不会重定向循环", async () => {
|
|
196
173
|
const snapshot = accessSnapshot();
|
|
197
174
|
let accessRefreshes = 0;
|
|
@@ -232,165 +209,5 @@ describe("Web 路由强制访问", () => {
|
|
|
232
209
|
|
|
233
210
|
expect(router.currentRoute.value.name).toBe("runtime-license");
|
|
234
211
|
});
|
|
235
|
-
|
|
236
|
-
test("未认证直访受保护页面会保存安全 returnTo 并进入登录页", async () => {
|
|
237
|
-
const router = createAppRouter(
|
|
238
|
-
createMemoryHistory(),
|
|
239
|
-
provider(accessSnapshot()),
|
|
240
|
-
licenseProvider(),
|
|
241
|
-
authProvider("anonymous"),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
await router.push("/admin/system/users?page=2");
|
|
245
|
-
|
|
246
|
-
expect(router.currentRoute.value.name).toBe("login");
|
|
247
|
-
expect(router.currentRoute.value.query.returnTo).toBe(
|
|
248
|
-
"/admin/system/users?page=2",
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("已认证用户访问登录页只接受站内绝对 returnTo", async () => {
|
|
253
|
-
const router = createAppRouter(
|
|
254
|
-
createMemoryHistory(),
|
|
255
|
-
provider(accessSnapshot()),
|
|
256
|
-
licenseProvider(),
|
|
257
|
-
authProvider(),
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
await router.push({
|
|
261
|
-
name: "login",
|
|
262
|
-
query: { returnTo: "https://evil.test" },
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
expect(router.currentRoute.value.path).toBe("/");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("管理员重置后的账号只能进入强制改密页且不会重定向循环", async () => {
|
|
269
|
-
const session = authProvider();
|
|
270
|
-
session.actor.credentialState = "must-change-password";
|
|
271
|
-
const router = createAppRouter(
|
|
272
|
-
createMemoryHistory(),
|
|
273
|
-
provider(accessSnapshot()),
|
|
274
|
-
licenseProvider(),
|
|
275
|
-
session,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
await router.push("/admin/system/users");
|
|
279
|
-
expect(router.currentRoute.value.name).toBe("change-password");
|
|
280
|
-
|
|
281
|
-
await router.push("/change-password");
|
|
282
|
-
expect(router.currentRoute.value.name).toBe("change-password");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("会话服务异常不会被误判成未登录", async () => {
|
|
286
|
-
const router = createAppRouter(
|
|
287
|
-
createMemoryHistory(),
|
|
288
|
-
provider(accessSnapshot()),
|
|
289
|
-
licenseProvider(),
|
|
290
|
-
{
|
|
291
|
-
ensureInitialized: async () => Promise.reject(new Error("offline")),
|
|
292
|
-
clear() {},
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
await router.push("/admin/system/users");
|
|
297
|
-
|
|
298
|
-
expect(router.currentRoute.value.name).toBe("access-denied");
|
|
299
|
-
expect(router.currentRoute.value.query.reason).toBe("session-unavailable");
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("会话过期引发的 401 才会清退到登录页", async () => {
|
|
303
|
-
const router = createAppRouter(
|
|
304
|
-
createMemoryHistory(),
|
|
305
|
-
provider(accessSnapshot()),
|
|
306
|
-
{ refresh: async () => Promise.reject(new ApiError("expired", 401)) },
|
|
307
|
-
authProvider(),
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
await router.push("/admin/system/license");
|
|
311
|
-
|
|
312
|
-
expect(router.currentRoute.value.name).toBe("login");
|
|
313
|
-
expect(router.currentRoute.value.query.returnTo).toBe(
|
|
314
|
-
"/admin/system/license",
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe("安全 returnTo", () => {
|
|
320
|
-
test("只接受应用内绝对路径并排除登录页自身", () => {
|
|
321
|
-
expect(safeReturnTo("/admin/system/users?tab=active#top")).toBe(
|
|
322
|
-
"/admin/system/users?tab=active#top",
|
|
323
|
-
);
|
|
324
|
-
expect(safeReturnTo("https://evil.test/admin/system/users")).toBe("/");
|
|
325
|
-
expect(safeReturnTo("//evil.test/admin/system/users")).toBe("/");
|
|
326
|
-
expect(safeReturnTo("/\\evil.test/admin/system/users")).toBe("/");
|
|
327
|
-
expect(safeReturnTo("/login?returnTo=/admin/system/users")).toBe("/");
|
|
328
|
-
});
|
|
212
|
+
// @windy-module system.license end
|
|
329
213
|
});
|
|
330
|
-
|
|
331
|
-
function provider(snapshot: WebAccessSnapshot) {
|
|
332
|
-
return { refresh: async () => snapshot };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function licenseProvider(restricted = false) {
|
|
336
|
-
const snapshot = licenseSnapshot(restricted);
|
|
337
|
-
return { refresh: async () => snapshot };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function licenseSnapshot(restricted = false): PlatformLicenseSnapshot {
|
|
341
|
-
return {
|
|
342
|
-
status: restricted ? "expired" : "valid",
|
|
343
|
-
restricted,
|
|
344
|
-
reminder: { level: "none" },
|
|
345
|
-
canViewDetails: false,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function authProvider(phase: "authenticated" | "anonymous" = "authenticated") {
|
|
350
|
-
let currentPhase = phase;
|
|
351
|
-
const actor = accessSnapshot().actor;
|
|
352
|
-
return {
|
|
353
|
-
actor,
|
|
354
|
-
ensureInitialized: async () => ({
|
|
355
|
-
phase: currentPhase,
|
|
356
|
-
actor: currentPhase === "authenticated" ? actor : undefined,
|
|
357
|
-
}),
|
|
358
|
-
clear() {
|
|
359
|
-
currentPhase = "anonymous";
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function accessSnapshot(): WebAccessSnapshot {
|
|
365
|
-
return {
|
|
366
|
-
actor: {
|
|
367
|
-
id: "user_1",
|
|
368
|
-
type: "user",
|
|
369
|
-
name: "审计员",
|
|
370
|
-
roleCodes: ["auditor"],
|
|
371
|
-
permissionKeys: [
|
|
372
|
-
"system.admin.access",
|
|
373
|
-
"system.audit.read",
|
|
374
|
-
"system.feature.read",
|
|
375
|
-
],
|
|
376
|
-
},
|
|
377
|
-
menus: [],
|
|
378
|
-
permissions: [],
|
|
379
|
-
features: [
|
|
380
|
-
{
|
|
381
|
-
key: "system.audit",
|
|
382
|
-
enabled: true,
|
|
383
|
-
visible: "visible",
|
|
384
|
-
allowed: true,
|
|
385
|
-
reason: "allowed",
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
key: "system.rbac",
|
|
389
|
-
enabled: true,
|
|
390
|
-
visible: "visible",
|
|
391
|
-
allowed: true,
|
|
392
|
-
reason: "allowed",
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
};
|
|
396
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { systemFoundationModule } from "@windy/modules";
|
|
3
|
+
import { routes } from "./index";
|
|
4
|
+
|
|
5
|
+
describe("Manifest 菜单与 Router 契约", () => {
|
|
6
|
+
test("路由默认要求登录,只有显式 public 的页面允许匿名访问", () => {
|
|
7
|
+
const publicRoutes = routes.filter((route) => route.meta?.public === true);
|
|
8
|
+
|
|
9
|
+
expect(publicRoutes.map((route) => route.path)).toEqual([
|
|
10
|
+
"/login",
|
|
11
|
+
"/service-unavailable",
|
|
12
|
+
]);
|
|
13
|
+
expect(routes.find((route) => route.path === "/")?.meta?.public).not.toBe(
|
|
14
|
+
true,
|
|
15
|
+
);
|
|
16
|
+
expect(
|
|
17
|
+
routes.find((route) => route.path === "/admin")?.meta?.public,
|
|
18
|
+
).not.toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("每个 Admin Route 都由真实页面实现且 Guard 来自 Manifest", () => {
|
|
22
|
+
const manifests = [systemFoundationModule()];
|
|
23
|
+
const definitions = manifests.flatMap((manifest) => manifest.adminRoutes);
|
|
24
|
+
const implementationByKey = new Map(
|
|
25
|
+
routes.flatMap((route) => {
|
|
26
|
+
const key = route.meta?.manifestRouteKey;
|
|
27
|
+
return typeof key === "string" ? [[key, route] as const] : [];
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect([...implementationByKey.keys()].sort()).toEqual(
|
|
32
|
+
definitions.map(({ key }) => key).sort(),
|
|
33
|
+
);
|
|
34
|
+
for (const definition of definitions) {
|
|
35
|
+
const route = implementationByKey.get(definition.key);
|
|
36
|
+
expect(route?.path).toBe(definition.path);
|
|
37
|
+
expect(route?.meta?.title).toBe(definition.title);
|
|
38
|
+
if (definition.directAccess !== "authenticated") {
|
|
39
|
+
expect(route?.meta?.permissionKey).toBe(definition.permissionKey);
|
|
40
|
+
expect(route?.meta?.featureKey).toBe(definition.featureKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
expect(definitions.some(({ path }) => path === "/admin/profile")).toBe(
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
expect(definitions.some(({ path }) => path === "/work-orders")).toBe(false);
|
|
47
|
+
expect(routes.some(({ path }) => path === "/work-orders")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WebAccessSnapshot } from "@/layout/navigation";
|
|
2
|
+
// @windy-module system.license begin
|
|
3
|
+
import type { PlatformLicenseSnapshot } from "@/services/license-api";
|
|
4
|
+
// @windy-module system.license end
|
|
5
|
+
|
|
6
|
+
export function provider(snapshot: WebAccessSnapshot) {
|
|
7
|
+
return { refresh: async () => snapshot };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// @windy-module system.license begin
|
|
11
|
+
export function licenseProvider(restricted = false) {
|
|
12
|
+
const snapshot = licenseSnapshot(restricted);
|
|
13
|
+
return { refresh: async () => snapshot };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function licenseSnapshot(restricted = false): PlatformLicenseSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
status: restricted ? "expired" : "valid",
|
|
19
|
+
restricted,
|
|
20
|
+
reminder: { level: "none" },
|
|
21
|
+
canViewDetails: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// @windy-module system.license end
|
|
25
|
+
|
|
26
|
+
export function authProvider(
|
|
27
|
+
phase: "authenticated" | "anonymous" = "authenticated",
|
|
28
|
+
) {
|
|
29
|
+
let currentPhase = phase;
|
|
30
|
+
const actor = accessSnapshot().actor;
|
|
31
|
+
return {
|
|
32
|
+
actor,
|
|
33
|
+
ensureInitialized: async () => ({
|
|
34
|
+
phase: currentPhase,
|
|
35
|
+
actor: currentPhase === "authenticated" ? actor : undefined,
|
|
36
|
+
}),
|
|
37
|
+
clear() {
|
|
38
|
+
currentPhase = "anonymous";
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function accessSnapshot(): WebAccessSnapshot {
|
|
44
|
+
return {
|
|
45
|
+
actor: {
|
|
46
|
+
id: "user_1",
|
|
47
|
+
type: "user",
|
|
48
|
+
name: "审计员",
|
|
49
|
+
roleCodes: ["auditor"],
|
|
50
|
+
permissionKeys: [
|
|
51
|
+
"system.admin.access",
|
|
52
|
+
"system.audit.read",
|
|
53
|
+
"system.feature.read",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
menus: [],
|
|
57
|
+
permissions: [],
|
|
58
|
+
features: [
|
|
59
|
+
{
|
|
60
|
+
key: "system.audit",
|
|
61
|
+
enabled: true,
|
|
62
|
+
visible: "visible",
|
|
63
|
+
allowed: true,
|
|
64
|
+
reason: "allowed",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "system.rbac",
|
|
68
|
+
enabled: true,
|
|
69
|
+
visible: "visible",
|
|
70
|
+
allowed: true,
|
|
71
|
+
reason: "allowed",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
package/template/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"prepare": "husky",
|
|
13
|
-
"dev": "bun run
|
|
13
|
+
"dev": "bun run scripts/dev.ts",
|
|
14
14
|
"dev:web": "bun run --cwd apps/web dev",
|
|
15
15
|
"dev:server": "bun run --cwd apps/server dev",
|
|
16
16
|
"dev:license": "bun run --cwd apps/license dev",
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
// @windy-module system.license begin
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
|
+
import { timestamp as licenseTimestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
import { idColumn as licenseIdColumn } from "./common.js";
|
|
3
5
|
// @windy-module system.license end
|
|
6
|
+
// @windy-module system.audit begin
|
|
7
|
+
import { timestamp as auditTimestamp } from "drizzle-orm/pg-core";
|
|
8
|
+
import { idColumn as auditIdColumn } from "./common.js";
|
|
9
|
+
// @windy-module system.audit end
|
|
4
10
|
import {
|
|
5
11
|
boolean,
|
|
6
12
|
index,
|
|
7
13
|
jsonb,
|
|
8
14
|
pgTable,
|
|
9
15
|
text,
|
|
10
|
-
timestamp,
|
|
11
16
|
// @windy-module system.license begin
|
|
12
17
|
uniqueIndex,
|
|
13
18
|
// @windy-module system.license end
|
|
14
19
|
varchar,
|
|
15
20
|
} from "drizzle-orm/pg-core";
|
|
16
|
-
import { auditColumns,
|
|
21
|
+
import { auditColumns, statusColumn } from "./common.js";
|
|
17
22
|
|
|
18
23
|
export const featureFlags = pgTable(
|
|
19
24
|
"feature_flags",
|
|
@@ -43,7 +48,7 @@ export const featureFlags = pgTable(
|
|
|
43
48
|
export const licenseOperators = pgTable(
|
|
44
49
|
"license_operators",
|
|
45
50
|
{
|
|
46
|
-
id:
|
|
51
|
+
id: licenseIdColumn().primaryKey(),
|
|
47
52
|
username: varchar("username", { length: 80 }).notNull(),
|
|
48
53
|
displayName: varchar("display_name", { length: 120 }).notNull(),
|
|
49
54
|
tokenHash: varchar("token_hash", { length: 128 }),
|
|
@@ -51,13 +56,13 @@ export const licenseOperators = pgTable(
|
|
|
51
56
|
role: varchar("role", { length: 40 }).notNull().default("readonly"),
|
|
52
57
|
permissions: jsonb("permissions").$type<string[]>().notNull(),
|
|
53
58
|
status: statusColumn().notNull(),
|
|
54
|
-
lastUsedAt:
|
|
55
|
-
createdAt:
|
|
59
|
+
lastUsedAt: licenseTimestamp("last_used_at", { withTimezone: true }),
|
|
60
|
+
createdAt: licenseTimestamp("created_at", { withTimezone: true })
|
|
56
61
|
.notNull()
|
|
57
62
|
.defaultNow(),
|
|
58
|
-
updatedAt:
|
|
63
|
+
updatedAt: licenseTimestamp("updated_at", { withTimezone: true }),
|
|
59
64
|
updatedBy: varchar("updated_by", { length: 64 }),
|
|
60
|
-
disabledAt:
|
|
65
|
+
disabledAt: licenseTimestamp("disabled_at", { withTimezone: true }),
|
|
61
66
|
disabledBy: varchar("disabled_by", { length: 64 }),
|
|
62
67
|
},
|
|
63
68
|
(table) => [
|
|
@@ -69,18 +74,18 @@ export const licenseOperators = pgTable(
|
|
|
69
74
|
export const licenseOperatorSessions = pgTable(
|
|
70
75
|
"license_operator_sessions",
|
|
71
76
|
{
|
|
72
|
-
id:
|
|
77
|
+
id: licenseIdColumn().primaryKey(),
|
|
73
78
|
operatorId: varchar("operator_id", { length: 64 })
|
|
74
79
|
.notNull()
|
|
75
80
|
.references(() => licenseOperators.id, { onDelete: "restrict" }),
|
|
76
81
|
tokenHash: varchar("token_hash", { length: 128 }).notNull(),
|
|
77
82
|
status: statusColumn().notNull().default("active"),
|
|
78
|
-
issuedAt:
|
|
83
|
+
issuedAt: licenseTimestamp("issued_at", { withTimezone: true })
|
|
79
84
|
.notNull()
|
|
80
85
|
.defaultNow(),
|
|
81
|
-
expiresAt:
|
|
82
|
-
revokedAt:
|
|
83
|
-
lastSeenAt:
|
|
86
|
+
expiresAt: licenseTimestamp("expires_at", { withTimezone: true }).notNull(),
|
|
87
|
+
revokedAt: licenseTimestamp("revoked_at", { withTimezone: true }),
|
|
88
|
+
lastSeenAt: licenseTimestamp("last_seen_at", { withTimezone: true }),
|
|
84
89
|
ip: varchar("ip", { length: 80 }),
|
|
85
90
|
userAgent: text("user_agent"),
|
|
86
91
|
},
|
|
@@ -95,7 +100,7 @@ export const licenseOperatorSessions = pgTable(
|
|
|
95
100
|
export const licenseKeys = pgTable(
|
|
96
101
|
"license_keys",
|
|
97
102
|
{
|
|
98
|
-
id:
|
|
103
|
+
id: licenseIdColumn().primaryKey(),
|
|
99
104
|
keyId: varchar("key_id", { length: 160 }).notNull(),
|
|
100
105
|
publicKeyPem: text("public_key_pem").notNull(),
|
|
101
106
|
algorithm: varchar("algorithm", { length: 40 })
|
|
@@ -103,11 +108,11 @@ export const licenseKeys = pgTable(
|
|
|
103
108
|
.default("ED25519"),
|
|
104
109
|
status: statusColumn().notNull(),
|
|
105
110
|
rotatedFromKeyId: varchar("rotated_from_key_id", { length: 160 }),
|
|
106
|
-
createdAt:
|
|
111
|
+
createdAt: licenseTimestamp("created_at", { withTimezone: true }).notNull(),
|
|
107
112
|
createdBy: varchar("created_by", { length: 64 }).notNull(),
|
|
108
|
-
retiredAt:
|
|
113
|
+
retiredAt: licenseTimestamp("retired_at", { withTimezone: true }),
|
|
109
114
|
retiredBy: varchar("retired_by", { length: 64 }),
|
|
110
|
-
retainUntil:
|
|
115
|
+
retainUntil: licenseTimestamp("retain_until", { withTimezone: true }),
|
|
111
116
|
},
|
|
112
117
|
(table) => [
|
|
113
118
|
uniqueIndex("uk_license_keys_key_id").on(table.keyId),
|
|
@@ -121,13 +126,13 @@ export const licenseKeys = pgTable(
|
|
|
121
126
|
export const licenseKeyCompatibilities = pgTable(
|
|
122
127
|
"license_key_compatibilities",
|
|
123
128
|
{
|
|
124
|
-
id:
|
|
129
|
+
id: licenseIdColumn().primaryKey(),
|
|
125
130
|
productCode: varchar("product_code", { length: 120 }).notNull(),
|
|
126
131
|
softwareVersion: varchar("software_version", { length: 80 }).notNull(),
|
|
127
132
|
publicKeyId: varchar("public_key_id", { length: 160 })
|
|
128
133
|
.notNull()
|
|
129
134
|
.references(() => licenseKeys.keyId, { onDelete: "restrict" }),
|
|
130
|
-
declaredAt:
|
|
135
|
+
declaredAt: licenseTimestamp("declared_at", { withTimezone: true })
|
|
131
136
|
.notNull()
|
|
132
137
|
.defaultNow(),
|
|
133
138
|
declaredBy: varchar("declared_by", { length: 64 }).notNull(),
|
|
@@ -149,12 +154,12 @@ export const licenseKeyCompatibilities = pgTable(
|
|
|
149
154
|
export const licenseGovernanceEvents = pgTable(
|
|
150
155
|
"license_governance_events",
|
|
151
156
|
{
|
|
152
|
-
id:
|
|
157
|
+
id: licenseIdColumn().primaryKey(),
|
|
153
158
|
type: varchar("type", { length: 100 }).notNull(),
|
|
154
159
|
resourceType: varchar("resource_type", { length: 80 }).notNull(),
|
|
155
160
|
resourceId: varchar("resource_id", { length: 160 }).notNull(),
|
|
156
161
|
actorId: varchar("actor_id", { length: 64 }).notNull(),
|
|
157
|
-
occurredAt:
|
|
162
|
+
occurredAt: licenseTimestamp("occurred_at", { withTimezone: true })
|
|
158
163
|
.notNull()
|
|
159
164
|
.defaultNow(),
|
|
160
165
|
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
|
@@ -178,7 +183,7 @@ export const licenseGovernanceEvents = pgTable(
|
|
|
178
183
|
export const auditLogs = pgTable(
|
|
179
184
|
"audit_logs",
|
|
180
185
|
{
|
|
181
|
-
id:
|
|
186
|
+
id: auditIdColumn().primaryKey(),
|
|
182
187
|
actorId: varchar("actor_id", { length: 64 }),
|
|
183
188
|
action: varchar("action", { length: 120 }).notNull(),
|
|
184
189
|
resource: varchar("resource", { length: 160 }).notNull(),
|
|
@@ -186,7 +191,7 @@ export const auditLogs = pgTable(
|
|
|
186
191
|
ip: varchar("ip", { length: 64 }),
|
|
187
192
|
userAgent: text("user_agent"),
|
|
188
193
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
189
|
-
createdAt:
|
|
194
|
+
createdAt: auditTimestamp("created_at", { withTimezone: true })
|
|
190
195
|
.notNull()
|
|
191
196
|
.defaultNow(),
|
|
192
197
|
},
|
|
@@ -1,43 +1,57 @@
|
|
|
1
|
+
// 即使两个可选模块都被裁剪,也要保持该文件可由 schema barrel 导出。
|
|
2
|
+
export {};
|
|
3
|
+
|
|
4
|
+
// @windy-module system.configuration begin
|
|
1
5
|
import type { ModuleConfigDiffEntry, ModuleConfigValues } from "@windy/shared";
|
|
2
6
|
import {
|
|
3
7
|
index,
|
|
4
8
|
integer,
|
|
5
9
|
jsonb,
|
|
6
|
-
pgTable,
|
|
7
|
-
text,
|
|
10
|
+
pgTable as configTable,
|
|
8
11
|
timestamp,
|
|
9
12
|
uniqueIndex,
|
|
10
|
-
varchar,
|
|
13
|
+
varchar as configVarchar,
|
|
14
|
+
} from "drizzle-orm/pg-core";
|
|
15
|
+
// @windy-module system.configuration end
|
|
16
|
+
// @windy-module system.settings begin
|
|
17
|
+
import {
|
|
18
|
+
pgTable as settingsTable,
|
|
19
|
+
text,
|
|
20
|
+
varchar as settingsVarchar,
|
|
11
21
|
} from "drizzle-orm/pg-core";
|
|
12
22
|
import { auditColumns } from "./common.js";
|
|
23
|
+
// @windy-module system.settings end
|
|
13
24
|
|
|
14
25
|
// @windy-module system.settings begin
|
|
15
|
-
export const platformBrandingSettings =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
26
|
+
export const platformBrandingSettings = settingsTable(
|
|
27
|
+
"platform_branding_settings",
|
|
28
|
+
{
|
|
29
|
+
id: settingsVarchar("id", { length: 64 }).primaryKey(),
|
|
30
|
+
softwareName: settingsVarchar("software_name", { length: 120 }).notNull(),
|
|
31
|
+
logoUrl: text("logo_url"),
|
|
32
|
+
description: settingsVarchar("description", { length: 500 }),
|
|
33
|
+
...auditColumns,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
22
36
|
// @windy-module system.settings end
|
|
23
37
|
|
|
24
38
|
// @windy-module system.configuration begin
|
|
25
|
-
export const moduleConfigVersions =
|
|
39
|
+
export const moduleConfigVersions = configTable(
|
|
26
40
|
"module_config_versions",
|
|
27
41
|
{
|
|
28
|
-
id:
|
|
29
|
-
moduleKey:
|
|
42
|
+
id: configVarchar("id", { length: 64 }).primaryKey(),
|
|
43
|
+
moduleKey: configVarchar("module_key", { length: 80 }).notNull(),
|
|
30
44
|
version: integer("version").notNull(),
|
|
31
45
|
values: jsonb("values").$type<ModuleConfigValues>().notNull(),
|
|
32
46
|
diff: jsonb("diff").$type<ModuleConfigDiffEntry[]>().notNull(),
|
|
33
|
-
applyStatus:
|
|
34
|
-
applyError:
|
|
47
|
+
applyStatus: configVarchar("apply_status", { length: 24 }).notNull(),
|
|
48
|
+
applyError: configVarchar("apply_error", { length: 500 }),
|
|
35
49
|
rollbackOfVersion: integer("rollback_of_version"),
|
|
36
|
-
reason:
|
|
50
|
+
reason: configVarchar("reason", { length: 500 }),
|
|
37
51
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
38
52
|
.notNull()
|
|
39
53
|
.defaultNow(),
|
|
40
|
-
createdBy:
|
|
54
|
+
createdBy: configVarchar("created_by", { length: 64 }).notNull(),
|
|
41
55
|
appliedAt: timestamp("applied_at", { withTimezone: true }),
|
|
42
56
|
},
|
|
43
57
|
(table) => [
|
|
@@ -33,6 +33,7 @@ export const systemAdminRoutes: ModuleManifest["adminRoutes"] = [
|
|
|
33
33
|
"system.department.read",
|
|
34
34
|
"system.rbac",
|
|
35
35
|
),
|
|
36
|
+
// @windy-module system.settings begin
|
|
36
37
|
{
|
|
37
38
|
...route(
|
|
38
39
|
"system.settings",
|
|
@@ -44,6 +45,7 @@ export const systemAdminRoutes: ModuleManifest["adminRoutes"] = [
|
|
|
44
45
|
),
|
|
45
46
|
directAccess: "admin-admission",
|
|
46
47
|
},
|
|
48
|
+
// @windy-module system.settings end
|
|
47
49
|
route(
|
|
48
50
|
"system.menu",
|
|
49
51
|
"/admin/system/menus",
|