@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ysgroup/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export { useYsStore } from "./store";
5
5
  export {
6
6
  YsRest,
7
7
  YsRestError,
8
+ type YsAdminRpcCall,
8
9
  type DeleteBlocker,
9
10
  type DeleteImpact,
10
11
  type RevealedCredential,
package/src/rest.ts CHANGED
@@ -1,4 +1,13 @@
1
- /* @ysgroup/core —— REST 客户端(管理接口,带 JWT)。 */
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(table: string, id: string, body: {
89
- fields: string[];
90
- password: string;
91
- reason: string;
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(table: string, id: string, body: {
99
- credentials: Record<string, string>;
100
- password: string;
101
- reason: string;
102
- }) {
103
- return this.request<{ updated_fields: string[] }>(
104
- "POST",
105
- `/api/tables/${table}/${id}/credentials/update`,
106
- body,
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
- relation: "apps" | "strategies") {
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
- relation: "apps" | "strategies", ids: string[]) {
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 && String(user.value._id) === d.user_id) void logout();
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
- void logout();
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
  };