@ysgroup/core 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/rest.ts +105 -27
- package/src/store.ts +24 -6
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/rest.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
/* @ysgroup/core ——
|
|
1
|
+
/* @ysgroup/core —— 管理接口客户端(支持 HTTP 或统一 WS RPC 传输)。 */
|
|
2
|
+
|
|
3
|
+
export type YsAdminRpcCall = (data: {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
access_token: string;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
upload?: { name: string; content_type: string; base64: string };
|
|
9
|
+
user_agent: string;
|
|
10
|
+
}) => Promise<unknown>;
|
|
2
11
|
|
|
3
12
|
export class YsRestError extends Error {
|
|
4
13
|
constructor(
|
|
@@ -33,9 +42,23 @@ export class YsRest {
|
|
|
33
42
|
constructor(
|
|
34
43
|
private baseUrl: string,
|
|
35
44
|
private getToken: () => string,
|
|
45
|
+
private rpcCall?: YsAdminRpcCall,
|
|
36
46
|
) {}
|
|
37
47
|
|
|
38
48
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
49
|
+
if (this.rpcCall) {
|
|
50
|
+
try {
|
|
51
|
+
return await this.rpcCall({
|
|
52
|
+
method,
|
|
53
|
+
path,
|
|
54
|
+
access_token: this.getToken(),
|
|
55
|
+
body,
|
|
56
|
+
user_agent: navigator.userAgent,
|
|
57
|
+
}) as T;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw rpcError(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
40
63
|
method,
|
|
41
64
|
headers: {
|
|
@@ -59,6 +82,23 @@ export class YsRest {
|
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
private async upload<T>(path: string, file: File): Promise<T> {
|
|
85
|
+
if (this.rpcCall) {
|
|
86
|
+
try {
|
|
87
|
+
return await this.rpcCall({
|
|
88
|
+
method: "POST",
|
|
89
|
+
path,
|
|
90
|
+
access_token: this.getToken(),
|
|
91
|
+
upload: {
|
|
92
|
+
name: file.name,
|
|
93
|
+
content_type: file.type || "application/octet-stream",
|
|
94
|
+
base64: await fileToBase64(file),
|
|
95
|
+
},
|
|
96
|
+
user_agent: navigator.userAgent,
|
|
97
|
+
}) as T;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw rpcError(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
62
102
|
const form = new FormData();
|
|
63
103
|
form.append("upload", file);
|
|
64
104
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
@@ -73,6 +113,10 @@ export class YsRest {
|
|
|
73
113
|
return (await resp.json()) as T;
|
|
74
114
|
}
|
|
75
115
|
|
|
116
|
+
raw<T>(method: string, path: string, body?: unknown) {
|
|
117
|
+
return this.request<T>(method, path, body);
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
list<T = Record<string, unknown>>(table: string) {
|
|
77
121
|
return this.request<T[]>("GET", `/api/tables/${table}`);
|
|
78
122
|
}
|
|
@@ -85,26 +129,30 @@ export class YsRest {
|
|
|
85
129
|
deleteImpact(table: string, id: string) {
|
|
86
130
|
return this.request<DeleteImpact>("GET", `/api/tables/${table}/${id}/delete-impact`);
|
|
87
131
|
}
|
|
88
|
-
revealCredentials(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
revealCredentials(
|
|
133
|
+
table: string,
|
|
134
|
+
id: string,
|
|
135
|
+
body: {
|
|
136
|
+
fields: string[];
|
|
137
|
+
password: string;
|
|
138
|
+
reason: string;
|
|
139
|
+
},
|
|
140
|
+
) {
|
|
93
141
|
return this.request<{
|
|
94
142
|
credentials: Record<string, RevealedCredential>;
|
|
95
143
|
expires_in: number;
|
|
96
144
|
}>("POST", `/api/tables/${table}/${id}/credentials/reveal`, body);
|
|
97
145
|
}
|
|
98
|
-
updateCredentials(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
146
|
+
updateCredentials(
|
|
147
|
+
table: string,
|
|
148
|
+
id: string,
|
|
149
|
+
body: {
|
|
150
|
+
credentials: Record<string, string>;
|
|
151
|
+
password: string;
|
|
152
|
+
reason: string;
|
|
153
|
+
},
|
|
154
|
+
) {
|
|
155
|
+
return this.request<{ updated_fields: string[] }>("POST", `/api/tables/${table}/${id}/credentials/update`, body);
|
|
108
156
|
}
|
|
109
157
|
listApps() {
|
|
110
158
|
return this.request<Record<string, unknown>[]>("GET", "/api/apps");
|
|
@@ -186,22 +234,23 @@ export class YsRest {
|
|
|
186
234
|
owner_ids: ownerIds,
|
|
187
235
|
});
|
|
188
236
|
}
|
|
189
|
-
getRelations(entity: "owners" | "accounts", id: string,
|
|
190
|
-
|
|
191
|
-
return this.request<{ ids: string[] }>(
|
|
192
|
-
"GET", `/api/${entity}/${id}/relations/${relation}`,
|
|
193
|
-
);
|
|
237
|
+
getRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies") {
|
|
238
|
+
return this.request<{ ids: string[] }>("GET", `/api/${entity}/${id}/relations/${relation}`);
|
|
194
239
|
}
|
|
195
|
-
setRelations(entity: "owners" | "accounts", id: string,
|
|
196
|
-
|
|
197
|
-
return this.request<{ ids: string[] }>(
|
|
198
|
-
"PUT", `/api/${entity}/${id}/relations/${relation}`, { ids },
|
|
199
|
-
);
|
|
240
|
+
setRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies", ids: string[]) {
|
|
241
|
+
return this.request<{ ids: string[] }>("PUT", `/api/${entity}/${id}/relations/${relation}`, { ids });
|
|
200
242
|
}
|
|
201
243
|
listOwnerFiles(ownerId: string) {
|
|
202
244
|
return this.request<Record<string, unknown>[]>("GET", `/api/owners/${ownerId}/files`);
|
|
203
245
|
}
|
|
204
|
-
downloadFile(fileId: string) {
|
|
246
|
+
async downloadFile(fileId: string) {
|
|
247
|
+
if (this.rpcCall) {
|
|
248
|
+
const result = await this.request<{
|
|
249
|
+
base64: string;
|
|
250
|
+
content_type: string;
|
|
251
|
+
}>("GET", `/api/files/${fileId}/download`);
|
|
252
|
+
return base64ToBlob(result.base64, result.content_type);
|
|
253
|
+
}
|
|
205
254
|
return fetch(`${this.baseUrl}/api/files/${fileId}/download`, {
|
|
206
255
|
headers: { Authorization: `Bearer ${this.getToken()}` },
|
|
207
256
|
}).then(async (resp) => {
|
|
@@ -214,6 +263,35 @@ export class YsRest {
|
|
|
214
263
|
}
|
|
215
264
|
}
|
|
216
265
|
|
|
266
|
+
function rpcError(error: unknown): YsRestError {
|
|
267
|
+
const message = String(error).replace(/^Error:\s*/, "");
|
|
268
|
+
const match = message.match(/^\[(\d+)\]\s*(.*)$/s);
|
|
269
|
+
if (!match) return new YsRestError(500, message);
|
|
270
|
+
let detail: unknown = match[2];
|
|
271
|
+
try {
|
|
272
|
+
detail = JSON.parse(match[2]);
|
|
273
|
+
} catch {
|
|
274
|
+
// 普通文本错误保持原文。
|
|
275
|
+
}
|
|
276
|
+
return new YsRestError(Number(match[1]), detail);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fileToBase64(file: File): Promise<string> {
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const reader = new FileReader();
|
|
282
|
+
reader.onerror = () => reject(reader.error ?? new Error("读取上传文件失败"));
|
|
283
|
+
reader.onload = () => resolve(String(reader.result).split(",", 2)[1] ?? "");
|
|
284
|
+
reader.readAsDataURL(file);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function base64ToBlob(value: string, contentType: string): Blob {
|
|
289
|
+
const binary = atob(value);
|
|
290
|
+
const bytes = new Uint8Array(binary.length);
|
|
291
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
292
|
+
return new Blob([bytes], { type: contentType });
|
|
293
|
+
}
|
|
294
|
+
|
|
217
295
|
export interface TopologyNode {
|
|
218
296
|
name: string;
|
|
219
297
|
title: string;
|
package/src/store.ts
CHANGED
|
@@ -19,13 +19,14 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
19
19
|
const refreshToken = ref<string>(localStorage.getItem(LS_REFRESH) ?? "");
|
|
20
20
|
const connected = ref(false);
|
|
21
21
|
const appName = ref("");
|
|
22
|
+
const sessionMessage = ref("");
|
|
22
23
|
/** 差量订阅的表数据:table -> Map<id, doc> */
|
|
23
24
|
const tables = ref<Record<string, Map<string, Record<string, unknown>>>>({});
|
|
24
25
|
|
|
25
26
|
let ws: YsWsClient | null = null;
|
|
26
27
|
let renewTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
28
|
|
|
28
|
-
const isLoggedIn = computed(() => !!accessToken.value);
|
|
29
|
+
const isLoggedIn = computed(() => !!accessToken.value && !!user.value);
|
|
29
30
|
|
|
30
31
|
function tableList(table: string): Record<string, unknown>[] {
|
|
31
32
|
return Array.from(tables.value[table]?.values() ?? []);
|
|
@@ -41,7 +42,7 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
41
42
|
ws.on("__disconnected", () => (connected.value = false));
|
|
42
43
|
ws.on("snapshot", (d: unknown) => applySnapshot(d as SnapshotData));
|
|
43
44
|
ws.on("delta", (d: unknown) => applyDelta(d as DeltaData));
|
|
44
|
-
ws.on("user_revoked", (d: unknown) => onRevoked(d as { user_id: string }));
|
|
45
|
+
ws.on("user_revoked", (d: unknown) => onRevoked(d as { user_id: string; reason?: string }));
|
|
45
46
|
ws.on("resync", () => {}); // 前端订阅本子系统后端,resync 由后端 SDK 处理
|
|
46
47
|
ws.start();
|
|
47
48
|
}
|
|
@@ -78,12 +79,28 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
78
79
|
tables.value = { ...tables.value, [d.table]: new Map(map) };
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function onRevoked(d: { user_id: string }) {
|
|
82
|
-
if (user.value
|
|
82
|
+
function onRevoked(d: { user_id: string; reason?: string }) {
|
|
83
|
+
if (!user.value || String(user.value._id) !== d.user_id) return;
|
|
84
|
+
clearSession();
|
|
85
|
+
sessionMessage.value = revokedMessage(d.reason);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function revokedMessage(reason = ""): string {
|
|
89
|
+
if (reason === "password_changed") {
|
|
90
|
+
return "登录密码已修改,您已从所有系统退出,请使用新密码重新登录。";
|
|
91
|
+
}
|
|
92
|
+
if (reason === "user_disabled" || reason === "disabled") {
|
|
93
|
+
return "账号已停用,请联系管理员。";
|
|
94
|
+
}
|
|
95
|
+
if (reason === "app_access_revoked") {
|
|
96
|
+
return "当前账号已无权访问该系统。";
|
|
97
|
+
}
|
|
98
|
+
return "登录状态已失效,请重新登录。";
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
async function login(username: string, password: string, otp = ""): Promise<void> {
|
|
86
102
|
if (!ws) throw new Error("未连接");
|
|
103
|
+
sessionMessage.value = "";
|
|
87
104
|
const result = (await ws.call("login", {
|
|
88
105
|
username,
|
|
89
106
|
password,
|
|
@@ -135,6 +152,7 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
function setSession(result: LoginResult) {
|
|
155
|
+
sessionMessage.value = "";
|
|
138
156
|
if (result.user) user.value = result.user;
|
|
139
157
|
accessToken.value = result.access_token;
|
|
140
158
|
refreshToken.value = result.refresh_token;
|
|
@@ -162,7 +180,7 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
162
180
|
localStorage.setItem(LS_REFRESH, result.refresh_token);
|
|
163
181
|
scheduleRenew(result.access_expires_in);
|
|
164
182
|
} catch {
|
|
165
|
-
|
|
183
|
+
clearSession();
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
|
|
@@ -189,7 +207,7 @@ export const useYsStore = defineStore("ys", () => {
|
|
|
189
207
|
}
|
|
190
208
|
|
|
191
209
|
return {
|
|
192
|
-
user, accessToken, refreshToken, connected, isLoggedIn, tables,
|
|
210
|
+
user, accessToken, refreshToken, connected, isLoggedIn, sessionMessage, tables,
|
|
193
211
|
tableList, connect, login, loginByCode, waitUntilConnected, changeMyPassword, renew, logout,
|
|
194
212
|
client: () => ws,
|
|
195
213
|
};
|