@allbluecn/web-app 0.10.3 → 0.11.1

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.
Files changed (246) hide show
  1. package/package.json +24 -11
  2. package/public/favicon-dark.svg +1 -0
  3. package/public/favicon.ico +0 -0
  4. package/public/favicon.svg +1 -0
  5. package/public/logo/allblue-logo-app-icon.svg +7 -0
  6. package/public/logo/allblue-logo-dark-square.svg +4 -0
  7. package/public/logo/allblue-logo-dark.png +0 -0
  8. package/public/logo/allblue-logo-dark.svg +4 -0
  9. package/public/logo/allblue-logo-light-square.svg +4 -0
  10. package/public/logo/allblue-logo-light.png +0 -0
  11. package/public/logo/allblue-logo-light.svg +4 -0
  12. package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
  13. package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
  14. package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
  15. package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
  16. package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
  17. package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
  18. package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
  19. package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
  20. package/src/__tests__/security/xss-sanitize.test.ts +99 -0
  21. package/src/__tests__/setup.ts +9 -0
  22. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
  23. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
  24. package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
  25. package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
  26. package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
  27. package/src/components/auth/icon-input.tsx +9 -1
  28. package/src/components/auth/password-input.tsx +9 -1
  29. package/src/components/changelog/changelog-page.tsx +44 -1
  30. package/src/components/error/global-error-boundary.tsx +2 -2
  31. package/src/components/forms/app-field.tsx +3 -1
  32. package/src/components/knowledge/general-groups.ts +15 -0
  33. package/src/components/knowledge/kb-filter-popover.tsx +143 -0
  34. package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
  35. package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
  36. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
  37. package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
  38. package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
  39. package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
  40. package/src/components/notifications/settings-notifications.tsx +41 -1
  41. package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
  42. package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
  43. package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
  44. package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
  45. package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
  46. package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
  47. package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
  48. package/src/components/story/relations/general-groups.ts +1 -15
  49. package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
  50. package/src/components/story/story-list-view.tsx +7 -1
  51. package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
  52. package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
  53. package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
  54. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
  55. package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
  56. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
  57. package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
  58. package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
  59. package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
  60. package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
  61. package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
  62. package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
  63. package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
  64. package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
  65. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
  66. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
  67. package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
  68. package/src/lib/__tests__/edition-desktop.test.ts +23 -0
  69. package/src/lib/__tests__/msw-sample.test.ts +68 -0
  70. package/src/lib/__tests__/pgvector.test.ts +2 -2
  71. package/src/lib/app-version.ts +1 -1
  72. package/src/lib/auth/auth.config.ts +123 -99
  73. package/src/lib/changelog/sdk-versions.ts +21 -21
  74. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
  75. package/src/lib/collections/reference/workspaces-collection.ts +3 -1
  76. package/src/lib/edition-desktop.ts +22 -0
  77. package/src/lib/fetch-json.ts +23 -0
  78. package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
  79. package/src/lib/sanitize.ts +14 -1
  80. package/src/lib/session.ts +10 -2
  81. package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
  82. package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
  83. package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
  84. package/src/routeTree.gen.ts +114 -0
  85. package/src/routes/__root.tsx +20 -3
  86. package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
  87. package/src/routes/__tests__/-edition-routing.test.tsx +18 -5
  88. package/src/routes/__tests__/route-tree-structure.test.ts +66 -0
  89. package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
  90. package/src/routes/_login/changelog/confirm/route.tsx +28 -0
  91. package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
  92. package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
  93. package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
  94. package/src/routes/_login/login/route.lazy.tsx +4 -2
  95. package/src/routes/_login/register/route.lazy.tsx +28 -17
  96. package/src/routes/_login/register/route.tsx +8 -1
  97. package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
  98. package/src/routes/_login/schemas/-auth.ts +30 -7
  99. package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
  100. package/src/routes/_login/verify-email/route.tsx +28 -0
  101. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
  102. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  103. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
  104. package/src/routes/_nav/route.tsx +26 -0
  105. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
  106. package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
  107. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
  108. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  109. package/src/routes/_public/index/route.tsx +2 -1
  110. package/src/routes/api/auth/desktop-callback.ts +2 -2
  111. package/src/routes/api/billing-webhook.ts +4 -0
  112. package/src/routes/api/cron/changelog-release.ts +100 -0
  113. package/src/routes/api/cron/subscription-reminders.ts +110 -0
  114. package/src/routes/api.auth.$.ts +3 -5
  115. package/src/server/__tests__/agent-crud.test.ts +4 -4
  116. package/src/server/__tests__/auth-registration.test.ts +60 -0
  117. package/src/server/__tests__/chat-conversations.test.ts +1 -3
  118. package/src/server/__tests__/fetch-guard.test.ts +99 -0
  119. package/src/server/__tests__/permissions.test.ts +1 -3
  120. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  121. package/src/server/__tests__/skill-crud.test.ts +3 -3
  122. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
  123. package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
  124. package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
  125. package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
  126. package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
  127. package/src/server/ai-gateway/ai-output-guard.ts +77 -0
  128. package/src/server/ai-gateway/index.ts +5 -0
  129. package/src/server/ai-gateway/kb-context-guard.ts +46 -0
  130. package/src/server/ai-gateway/llm-fence.ts +82 -0
  131. package/src/server/ai-gateway/persistence/run-store.ts +19 -0
  132. package/src/server/ai-gateway/story-prompt.ts +4 -2
  133. package/src/server/ai-gateway/tools/definitions.ts +12 -6
  134. package/src/server/ai-gateway/tools/server.ts +97 -54
  135. package/src/server/auth-registration.ts +40 -0
  136. package/src/server/billing-email.ts +39 -0
  137. package/src/server/builtin-project.ts +21 -0
  138. package/src/server/email/__tests__/deliverability.test.ts +68 -0
  139. package/src/server/email/__tests__/index.test.ts +32 -0
  140. package/src/server/email/__tests__/outbox.test.tsx +129 -0
  141. package/src/server/email/__tests__/product-templates.test.tsx +106 -0
  142. package/src/server/email/__tests__/registry.test.tsx +50 -0
  143. package/src/server/email/deliverability.ts +57 -0
  144. package/src/server/email/drivers/console.ts +11 -0
  145. package/src/server/email/drivers/resend.ts +31 -0
  146. package/src/server/email/drivers/smtp.ts +27 -0
  147. package/src/server/email/index.ts +56 -0
  148. package/src/server/email/outbox.ts +141 -0
  149. package/src/server/email/preferences.ts +20 -0
  150. package/src/server/email/registry.ts +38 -0
  151. package/src/server/email/sender.ts +36 -13
  152. package/src/server/email/templates/_base/button.tsx +12 -0
  153. package/src/server/email/templates/_base/email-layout.tsx +117 -0
  154. package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
  155. package/src/server/email/templates/_base/theme.ts +22 -0
  156. package/src/server/email/templates/auth/reset-password.tsx +58 -0
  157. package/src/server/email/templates/auth/verify-email.tsx +59 -0
  158. package/src/server/email/templates/auth/welcome.tsx +49 -0
  159. package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
  160. package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
  161. package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
  162. package/src/server/email/templates/notification/mirror.tsx +50 -0
  163. package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
  164. package/src/server/email/templates/product/changelog-release.tsx +65 -0
  165. package/src/server/email/templates/team/apply-notify.tsx +53 -0
  166. package/src/server/email/templates/team/approved.tsx +53 -0
  167. package/src/server/email/templates/team/invite.tsx +57 -0
  168. package/src/server/fetch-guard.ts +170 -0
  169. package/src/server/middlewares/security-headers.ts +122 -0
  170. package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
  171. package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
  172. package/src/server/notifications/notification-service.ts +71 -1
  173. package/src/server/notifications/verify-reminder.ts +55 -0
  174. package/src/server/oauth-config.ts +59 -0
  175. package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
  176. package/src/server/serverFns/__tests__/billing.test.ts +263 -0
  177. package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
  178. package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
  179. package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
  180. package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
  181. package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
  182. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
  183. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
  184. package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
  185. package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
  186. package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
  187. package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
  188. package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
  189. package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
  190. package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
  191. package/src/server/serverFns/__tests__/team-invitation.test.ts +5 -5
  192. package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
  193. package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
  194. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
  195. package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
  196. package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
  197. package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
  198. package/src/server/serverFns/auth/email-verification.ts +132 -0
  199. package/src/server/serverFns/auth/forgot-password.ts +22 -2
  200. package/src/server/serverFns/auth/providers.ts +4 -3
  201. package/src/server/serverFns/auth/register.ts +63 -19
  202. package/src/server/serverFns/auth/registration-mode.ts +23 -0
  203. package/src/server/serverFns/auth/reset-password.ts +1 -1
  204. package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
  205. package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
  206. package/src/server/serverFns/changelog/subscribe.ts +89 -0
  207. package/src/server/serverFns/email-unsubscribe.ts +66 -0
  208. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
  209. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
  210. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
  211. package/src/server/serverFns/kb-doc-search.ts +111 -0
  212. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  213. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  214. package/src/server/serverFns/notifications.ts +10 -0
  215. package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
  216. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
  217. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
  218. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
  219. package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
  220. package/src/server/serverFns/story/kb-general-consts.ts +39 -0
  221. package/src/server/serverFns/story/kb-general.ts +30 -17
  222. package/src/server/serverFns/story/story-assoc.ts +13 -103
  223. package/src/server/serverFns/story/story-crud.ts +2 -5
  224. package/src/server/serverFns/story/story-groups.ts +4 -13
  225. package/src/server/serverFns/team.ts +50 -12
  226. package/src/server/serverFns/workspace-kb-utils.ts +138 -0
  227. package/src/server/serverFns/workspace-kb.ts +403 -0
  228. package/src/server/serverFns/workspace.ts +4 -1
  229. package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
  230. package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
  231. package/src/shared/ai-cli/adapter.ts +2 -2
  232. package/src/shared/ai-cli/cli-detector.ts +18 -9
  233. package/src/start.ts +2 -1
  234. package/src/styles/globals.css +114 -0
  235. package/src/types/jest-dom-vitest.d.ts +8 -7
  236. package/vite.shared.ts +105 -4
  237. package/public/file.svg +0 -1
  238. package/public/globe.svg +0 -1
  239. package/public/window.svg +0 -1
  240. package/src/lib/__tests__/cli-detector.test.ts +0 -80
  241. /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
  242. /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
  243. /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
  244. /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
  245. /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
  246. /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
@@ -84,7 +84,7 @@ export function WorkspaceMembersManager({ workspaceId, workspaceOwnerId, members
84
84
  const [teamUsers, setTeamUsers] = useState<TeamUser[]>([]);
85
85
  const [teamUsersLoading, setTeamUsersLoading] = useState(false);
86
86
  const [searchQuery, setSearchQuery] = useState("");
87
- const [selectedUserId, setSelectedUserId] = useState("");
87
+ const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
88
88
  const [memberRole, setMemberRole] = useState<"admin" | "member" | "viewer">("member");
89
89
  const [addingMember, setAddingMember] = useState(false);
90
90
  const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
@@ -107,29 +107,54 @@ export function WorkspaceMembersManager({ workspaceId, workspaceOwnerId, members
107
107
  u.email?.toLowerCase().includes(searchQuery.toLowerCase())
108
108
  );
109
109
 
110
+ const toggleUser = (userId: string) => {
111
+ setSelectedUserIds((prev) =>
112
+ prev.includes(userId) ? prev.filter((id) => id !== userId) : [...prev, userId]
113
+ );
114
+ };
115
+
110
116
  const openAddMember = () => {
111
- setSelectedUserId("");
117
+ setSelectedUserIds([]);
112
118
  setSearchQuery("");
113
119
  setMemberRole("member");
114
120
  setMemberDialogOpen(true);
115
121
  };
116
122
 
117
123
  const handleAddMember = async () => {
118
- if (!selectedUserId) {
124
+ if (selectedUserIds.length === 0) {
119
125
  toast.error(m.project_invite_selectMember());
120
126
  return;
121
127
  }
122
128
  setAddingMember(true);
129
+ const failed: string[] = [];
130
+ let lastError: string | null = null;
123
131
  try {
124
- await addWorkspaceMemberFn({
125
- data: { workspaceId, userId: selectedUserId, role: memberRole },
126
- });
127
- toast.success(m.workspace_home_memberRemoved());
128
- setMemberDialogOpen(false);
129
- setSelectedUserId("");
130
- onMembersUpdated();
131
- } catch (err) {
132
- toast.error(err instanceof Error ? err.message : m.workspace_home_addMemberFailed());
132
+ for (const userId of selectedUserIds) {
133
+ try {
134
+ await addWorkspaceMemberFn({
135
+ data: { workspaceId, userId, role: memberRole },
136
+ });
137
+ } catch (err) {
138
+ failed.push(userId);
139
+ lastError = err instanceof Error ? err.message : null;
140
+ }
141
+ }
142
+ if (failed.length === 0) {
143
+ toast.success(m.workspace_home_memberAdded());
144
+ setMemberDialogOpen(false);
145
+ setSelectedUserIds([]);
146
+ onMembersUpdated();
147
+ } else {
148
+ const failedNames = teamUsers
149
+ .filter((u) => failed.includes(u.id))
150
+ .map((u) => u.username)
151
+ .join(", ");
152
+ toast.error(
153
+ `${m.workspace_home_addMemberFailed()}: ${failedNames}${lastError ? ` (${lastError})` : ""}`
154
+ );
155
+ setSelectedUserIds(failed);
156
+ if (failed.length < selectedUserIds.length) onMembersUpdated();
157
+ }
133
158
  } finally {
134
159
  setAddingMember(false);
135
160
  }
@@ -222,32 +247,45 @@ export function WorkspaceMembersManager({ workspaceId, workspaceOwnerId, members
222
247
  {m.workspace_home_noMembers()}
223
248
  </div>
224
249
  ) : (
225
- filteredTeamUsers.map((user) => (
226
- <label
227
- key={user.id}
228
- className={`flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors ${
229
- selectedUserId === user.id ? "bg-primary/10" : "hover:bg-accent"
230
- }`}
231
- >
232
- <input
233
- type="radio"
234
- name="workspace-member"
235
- value={user.id}
236
- checked={selectedUserId === user.id}
237
- onChange={() => setSelectedUserId(user.id)}
238
- className="accent-primary"
239
- />
240
- <UserAvatar
241
- name={user.username}
242
- avatarConfig={user.avatarConfig as AvatarConfig | undefined}
243
- bgShape={user.bgShape as ShapeType | undefined}
244
- bgColor={user.bgColor ?? undefined}
245
- size="sm"
246
- />
247
- <span className="text-sm font-medium">{user.username}</span>
248
- <span className="text-xs text-muted-foreground ml-auto">{user.email}</span>
249
- </label>
250
- ))
250
+ filteredTeamUsers.map((user) => {
251
+ const isOwner = user.seatType === "owner";
252
+ const isSelected = selectedUserIds.includes(user.id);
253
+ return (
254
+ <label
255
+ key={user.id}
256
+ className={`flex items-center gap-3 px-3 py-2.5 transition-colors ${
257
+ isOwner
258
+ ? "cursor-not-allowed opacity-60"
259
+ : isSelected
260
+ ? "cursor-pointer bg-primary/10"
261
+ : "cursor-pointer hover:bg-accent"
262
+ }`}
263
+ >
264
+ <input
265
+ type="checkbox"
266
+ value={user.id}
267
+ checked={isSelected}
268
+ disabled={isOwner}
269
+ onChange={() => toggleUser(user.id)}
270
+ className="accent-primary"
271
+ />
272
+ <UserAvatar
273
+ name={user.username}
274
+ avatarConfig={user.avatarConfig as AvatarConfig | undefined}
275
+ bgShape={user.bgShape as ShapeType | undefined}
276
+ bgColor={user.bgColor ?? undefined}
277
+ size="sm"
278
+ />
279
+ <span className="text-sm font-medium">{user.username}</span>
280
+ {isOwner && (
281
+ <span className="text-xs text-muted-foreground">
282
+ ({m.workspace_home_role_owner()})
283
+ </span>
284
+ )}
285
+ <span className="text-xs text-muted-foreground ml-auto">{user.email}</span>
286
+ </label>
287
+ );
288
+ })
251
289
  )}
252
290
  </div>
253
291
  <div className="grid gap-2">
@@ -268,9 +306,9 @@ export function WorkspaceMembersManager({ workspaceId, workspaceOwnerId, members
268
306
  <Button variant="outline" onClick={() => setMemberDialogOpen(false)}>
269
307
  {m.common_button_cancel()}
270
308
  </Button>
271
- <Button onClick={handleAddMember} disabled={addingMember || !selectedUserId}>
309
+ <Button onClick={handleAddMember} disabled={addingMember || selectedUserIds.length === 0}>
272
310
  {addingMember ? <Loader2 className="size-4 animate-spin" /> : <Check className="size-4 mr-1.5" />}
273
- {m.projects_project_invite_sendInvite()}
311
+ {m.workspace_home_addMember()}
274
312
  </Button>
275
313
  </DialogFooter>
276
314
  </DialogContent>
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, afterEach, vi } from 'vitest'
2
+ import { isDesktopEdition } from '../edition-desktop'
3
+
4
+ afterEach(() => {
5
+ vi.unstubAllEnvs()
6
+ })
7
+
8
+ describe('isDesktopEdition', () => {
9
+ it('VITE_DEPLOY_TARGET=desktop → true', () => {
10
+ vi.stubEnv('VITE_DEPLOY_TARGET', 'desktop')
11
+ expect(isDesktopEdition()).toBe(true)
12
+ })
13
+
14
+ it('VITE_DEPLOY_TARGET=web → false', () => {
15
+ vi.stubEnv('VITE_DEPLOY_TARGET', 'web')
16
+ expect(isDesktopEdition()).toBe(false)
17
+ })
18
+
19
+ it('未注入构建常量 → false(Web 部署默认)', () => {
20
+ vi.stubEnv('VITE_DEPLOY_TARGET', '')
21
+ expect(isDesktopEdition()).toBe(false)
22
+ })
23
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it, afterEach } from "vitest"
2
+ import { setupServer } from "msw/node"
3
+ import { http, HttpResponse } from "msw"
4
+ import { fetchJson } from "../fetch-json"
5
+
6
+ // msw 样板:拦截 fetch 层,让接口测试与真实网络解耦。
7
+ // 后续 serverFn 客户端 / SDK fetch 测试统一复用此模式。
8
+ const server = setupServer(
9
+ http.get("https://api.example.com/models", () =>
10
+ HttpResponse.json([{ id: "gpt-x", displayName: "GPT X" }])
11
+ ),
12
+ http.post("https://api.example.com/echo", async ({ request }) =>
13
+ HttpResponse.json({ received: await request.json() })
14
+ ),
15
+ http.get("https://api.example.com/fail", () =>
16
+ HttpResponse.json({ error: "boom" }, { status: 500 })
17
+ )
18
+ )
19
+
20
+ describe("msw 接口 mock 样板", () => {
21
+ afterEach(() => {
22
+ server.resetHandlers()
23
+ server.close()
24
+ })
25
+
26
+ it("GET 拦截返回桩数据", async () => {
27
+ server.listen({ onUnhandledRequest: "error" })
28
+ const data = await fetchJson<{ id: string }[]>("https://api.example.com/models")
29
+ expect(data[0]!.id).toBe("gpt-x")
30
+ })
31
+
32
+ it("POST 请求体透传", async () => {
33
+ server.listen({ onUnhandledRequest: "error" })
34
+ const data = await fetchJson<{ received: { a: number } }>("https://api.example.com/echo", {
35
+ method: "POST",
36
+ body: JSON.stringify({ a: 1 }),
37
+ })
38
+ expect(data.received.a).toBe(1)
39
+ })
40
+
41
+ it("未拦截的请求直接报错(防测试悄悄打真网)", async () => {
42
+ server.listen({ onUnhandledRequest: "error" })
43
+ await expect(fetchJson("https://unhandled.example.com/x")).rejects.toThrow()
44
+ })
45
+
46
+ it("非 2xx 响应抛错,消息含状态码", async () => {
47
+ server.listen({ onUnhandledRequest: "error" })
48
+ await expect(fetchJson("https://api.example.com/fail")).rejects.toThrow(/HTTP 500/)
49
+ })
50
+
51
+ it("调用方传入的 AbortSignal 不被默认超时覆盖", async () => {
52
+ server.listen({ onUnhandledRequest: "error" })
53
+ const controller = new AbortController()
54
+ const data = await fetchJson<{ id: string }[]>("https://api.example.com/models", {
55
+ signal: controller.signal,
56
+ })
57
+ expect(data[0]!.id).toBe("gpt-x")
58
+ })
59
+
60
+ it("传入已 abort 的 signal 立即抛 AbortError", async () => {
61
+ server.listen({ onUnhandledRequest: "error" })
62
+ const controller = new AbortController()
63
+ controller.abort()
64
+ await expect(
65
+ fetchJson("https://api.example.com/models", { signal: controller.signal })
66
+ ).rejects.toThrow()
67
+ })
68
+ })
@@ -52,7 +52,7 @@ describe('searchByVector', () => {
52
52
 
53
53
  it('throws on illegal table name', async () => {
54
54
  await expect(searchByVector([0.1], 'users; DROP TABLE users; --'))
55
- .rejects.toMatchObject({ message: expect.stringMatching(/非法表名/) })
55
+ .rejects.toThrow(/非法表名/)
56
56
  })
57
57
 
58
58
  it('accepts allowed table names', async () => {
@@ -81,7 +81,7 @@ describe('insertEmbedding', () => {
81
81
 
82
82
  it('throws on illegal table name', async () => {
83
83
  await expect(insertEmbedding('users', 'id', 'c', [0.1]))
84
- .rejects.toMatchObject({ message: expect.stringMatching(/非法表名/) })
84
+ .rejects.toThrow(/非法表名/)
85
85
  })
86
86
 
87
87
  it('extraFields values are not concatenated into SQL string', async () => {
@@ -17,7 +17,7 @@
17
17
 
18
18
  // 版本号由 vite.shared.ts 构建时注入(__APP_VERSION__),无需运行时读取 package.json
19
19
  declare const __APP_VERSION__: string
20
- const APP_VERSION: string = __APP_VERSION__
20
+ export const APP_VERSION: string = __APP_VERSION__
21
21
 
22
22
  declare const __APP_UPDATE_DATE__: string
23
23
  const APP_UPDATE_DATE: string = __APP_UPDATE_DATE__
@@ -23,6 +23,7 @@ import { prisma } from '@allbluecn/database'
23
23
  import bcrypt from 'bcryptjs'
24
24
  import { getRandomConfig, presetBgColors } from '@/components/settings/personal-team/account/avatar/avatar-config'
25
25
  import { resolveIpLocation, getClientIp } from '@allbluecn/kernel'
26
+ import { getResolvedOAuthProviders } from '@/server/oauth-config'
26
27
 
27
28
  type CachedProfile = {
28
29
  username: string
@@ -92,6 +93,109 @@ function pickBgColor(seed: string) {
92
93
  return presetBgColors[Math.abs(hash) % presetBgColors.length]
93
94
  }
94
95
 
96
+ const credentialsProvider = CredentialsProvider({
97
+ name: 'credentials',
98
+ credentials: {
99
+ email: { label: 'Email', type: 'email' },
100
+ password: { label: 'Password', type: 'password' },
101
+ ssoToken: { label: 'SSO Token', type: 'text' },
102
+ },
103
+ async authorize(credentials, request) {
104
+ const creds = credentials as { email: string; password: string; remember?: string; stayLoggedIn?: string; ssoToken?: string }
105
+ if (creds.ssoToken) {
106
+ const { resolveSsoTokenUser } = await import('./sso-authorize')
107
+ const user = await resolveSsoTokenUser(creds.ssoToken)
108
+ if (!user) return null
109
+ return { id: user.id, email: user.email, name: user.name, role: user.role }
110
+ }
111
+ if (!creds.email || !creds.password) return null
112
+
113
+ const user = await prisma.user.findUnique({
114
+ where: { email: creds.email.toLowerCase().trim() },
115
+ select: { id: true, email: true, username: true, password: true, role: true, status: true },
116
+ })
117
+
118
+ // 用户不存在:userId 为外键,无法记录日志(保持现状)
119
+ if (!user) return null
120
+
121
+ // 安全提取 IP/UA(start-authjs 传递的 request 类型可能不标准,失败不阻塞登录)
122
+ let ip: string | null = null
123
+ let userAgent: string | null = null
124
+ try {
125
+ const headers = request?.headers as { get?: (name: string) => string | null } | undefined
126
+ if (headers && typeof headers.get === 'function') {
127
+ ip = getClientIp(request as Request)
128
+ userAgent = headers.get('user-agent') || null
129
+ }
130
+ } catch {
131
+ // 忽略:IP/UA 仅用于日志,获取失败不影响登录
132
+ }
133
+
134
+ // 安全记录登录日志(DB 异常不阻塞登录流程)
135
+ const recordLog = async (success: boolean, message?: string, location?: string) => {
136
+ try {
137
+ await prisma.loginLog.create({
138
+ data: {
139
+ userId: user.id,
140
+ ip,
141
+ userAgent,
142
+ location: location ?? null,
143
+ success,
144
+ ...(message ? { message } : {}),
145
+ },
146
+ })
147
+ } catch (err) {
148
+ console.error('[auth] 记录登录日志失败:', err instanceof Error ? err.message : String(err))
149
+ }
150
+ }
151
+
152
+ // 账号状态异常:记录失败原因
153
+ if (user.status === 'BANNED') {
154
+ await recordLog(false, '账号已封禁')
155
+ return null
156
+ }
157
+ if (user.status === 'INACTIVE') {
158
+ await recordLog(false, '账号未激活')
159
+ return null
160
+ }
161
+ if (user.status !== 'ACTIVE') {
162
+ await recordLog(false, '账号状态异常')
163
+ return null
164
+ }
165
+
166
+ const isValid = await bcrypt.compare(creds.password, user.password)
167
+ if (!isValid) {
168
+ await recordLog(false, '密码错误')
169
+ return null
170
+ }
171
+
172
+ // 登录成功:记录 IP/UA/地理位置
173
+ const location = await resolveIpLocation(ip)
174
+ await recordLog(true, undefined, location)
175
+ await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } })
176
+
177
+ const remember = creds.remember === 'true'
178
+ const stayLoggedIn = creds.stayLoggedIn === 'true'
179
+ return {
180
+ id: user.id,
181
+ email: user.email,
182
+ name: user.username,
183
+ role: user.role,
184
+ remember,
185
+ stayLoggedIn,
186
+ }
187
+ },
188
+ })
189
+
190
+ const GOOGLE_PROVIDER_BASE = {
191
+ authorization: {
192
+ url: 'https://accounts.google.com/o/oauth2/v2/auth',
193
+ params: { prompt: 'consent', access_type: 'offline', response_type: 'code' },
194
+ },
195
+ token: 'https://oauth2.googleapis.com/token',
196
+ userinfo: 'https://www.googleapis.com/oauth2/v3/userinfo',
197
+ } as const
198
+
95
199
  export const authConfig: StartAuthJSConfig = {
96
200
  basePath: '/api/auth',
97
201
  session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60 },
@@ -111,109 +215,12 @@ export const authConfig: StartAuthJSConfig = {
111
215
  },
112
216
  },
113
217
  providers: [
114
- CredentialsProvider({
115
- name: 'credentials',
116
- credentials: {
117
- email: { label: 'Email', type: 'email' },
118
- password: { label: 'Password', type: 'password' },
119
- ssoToken: { label: 'SSO Token', type: 'text' },
120
- },
121
- async authorize(credentials, request) {
122
- const creds = credentials as { email: string; password: string; remember?: string; stayLoggedIn?: string; ssoToken?: string }
123
- if (creds.ssoToken) {
124
- const { resolveSsoTokenUser } = await import('./sso-authorize')
125
- const user = await resolveSsoTokenUser(creds.ssoToken)
126
- if (!user) return null
127
- return { id: user.id, email: user.email, name: user.name, role: user.role }
128
- }
129
- if (!creds.email || !creds.password) return null
130
-
131
- const user = await prisma.user.findUnique({
132
- where: { email: creds.email },
133
- select: { id: true, email: true, username: true, password: true, role: true, status: true },
134
- })
135
-
136
- // 用户不存在:userId 为外键,无法记录日志(保持现状)
137
- if (!user) return null
138
-
139
- // 安全提取 IP/UA(start-authjs 传递的 request 类型可能不标准,失败不阻塞登录)
140
- let ip: string | null = null
141
- let userAgent: string | null = null
142
- try {
143
- const headers = request?.headers as { get?: (name: string) => string | null } | undefined
144
- if (headers && typeof headers.get === 'function') {
145
- ip = getClientIp(request as Request)
146
- userAgent = headers.get('user-agent') || null
147
- }
148
- } catch {
149
- // 忽略:IP/UA 仅用于日志,获取失败不影响登录
150
- }
151
-
152
- // 安全记录登录日志(DB 异常不阻塞登录流程)
153
- const recordLog = async (success: boolean, message?: string, location?: string) => {
154
- try {
155
- await prisma.loginLog.create({
156
- data: {
157
- userId: user.id,
158
- ip,
159
- userAgent,
160
- location: location ?? null,
161
- success,
162
- ...(message ? { message } : {}),
163
- },
164
- })
165
- } catch (err) {
166
- console.error('[auth] 记录登录日志失败:', err instanceof Error ? err.message : String(err))
167
- }
168
- }
169
-
170
- // 账号状态异常:记录失败原因
171
- if (user.status === 'BANNED') {
172
- await recordLog(false, '账号已封禁')
173
- return null
174
- }
175
- if (user.status === 'INACTIVE') {
176
- await recordLog(false, '账号未激活')
177
- return null
178
- }
179
- if (user.status !== 'ACTIVE') {
180
- await recordLog(false, '账号状态异常')
181
- return null
182
- }
183
-
184
- const isValid = await bcrypt.compare(creds.password, user.password)
185
- if (!isValid) {
186
- await recordLog(false, '密码错误')
187
- return null
188
- }
189
-
190
- // 登录成功:记录 IP/UA/地理位置
191
- const location = await resolveIpLocation(ip)
192
- await recordLog(true, undefined, location)
193
- await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } })
194
-
195
- const remember = creds.remember === 'true'
196
- const stayLoggedIn = creds.stayLoggedIn === 'true'
197
- return {
198
- id: user.id,
199
- email: user.email,
200
- name: user.username,
201
- role: user.role,
202
- remember,
203
- stayLoggedIn,
204
- }
205
- },
206
- }),
218
+ credentialsProvider,
207
219
  ...(process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET
208
220
  ? [GoogleProvider({
209
221
  clientId: process.env.AUTH_GOOGLE_ID,
210
222
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
211
- authorization: {
212
- url: 'https://accounts.google.com/o/oauth2/v2/auth',
213
- params: { prompt: 'consent', access_type: 'offline', response_type: 'code' },
214
- },
215
- token: 'https://oauth2.googleapis.com/token',
216
- userinfo: 'https://www.googleapis.com/oauth2/v3/userinfo',
223
+ ...GOOGLE_PROVIDER_BASE,
217
224
  })]
218
225
  : []),
219
226
  ],
@@ -381,3 +388,20 @@ export const authConfig: StartAuthJSConfig = {
381
388
  },
382
389
  },
383
390
  }
391
+
392
+ /** 异步工厂:DB OAuth 配置优先(60s 缓存),无则回落 env 静态 authConfig.providers */
393
+ export async function getAuthConfig(): Promise<StartAuthJSConfig> {
394
+ const resolved = await getResolvedOAuthProviders()
395
+ const google = resolved.find((p) => p.provider === 'google')
396
+ const providers = google
397
+ ? [
398
+ credentialsProvider,
399
+ GoogleProvider({
400
+ clientId: google.clientId,
401
+ clientSecret: google.clientSecret,
402
+ ...GOOGLE_PROVIDER_BASE,
403
+ }),
404
+ ]
405
+ : authConfig.providers
406
+ return { ...authConfig, providers }
407
+ }