@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.
- package/package.json +24 -11
- package/public/favicon-dark.svg +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +1 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.png +0 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.png +0 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
- package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/auth/icon-input.tsx +9 -1
- package/src/components/auth/password-input.tsx +9 -1
- package/src/components/changelog/changelog-page.tsx +44 -1
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/forms/app-field.tsx +3 -1
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/notifications/settings-notifications.tsx +41 -1
- package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/edition-desktop.test.ts +23 -0
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/app-version.ts +1 -1
- package/src/lib/auth/auth.config.ts +123 -99
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/edition-desktop.ts +22 -0
- package/src/lib/fetch-json.ts +23 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +10 -2
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routeTree.gen.ts +114 -0
- package/src/routes/__root.tsx +20 -3
- package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
- package/src/routes/__tests__/-edition-routing.test.tsx +18 -5
- package/src/routes/__tests__/route-tree-structure.test.ts +66 -0
- package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
- package/src/routes/_login/changelog/confirm/route.tsx +28 -0
- package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
- package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
- package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
- package/src/routes/_login/login/route.lazy.tsx +4 -2
- package/src/routes/_login/register/route.lazy.tsx +28 -17
- package/src/routes/_login/register/route.tsx +8 -1
- package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
- package/src/routes/_login/schemas/-auth.ts +30 -7
- package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
- package/src/routes/_login/verify-email/route.tsx +28 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +26 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/_public/index/route.tsx +2 -1
- package/src/routes/api/auth/desktop-callback.ts +2 -2
- package/src/routes/api/billing-webhook.ts +4 -0
- package/src/routes/api/cron/changelog-release.ts +100 -0
- package/src/routes/api/cron/subscription-reminders.ts +110 -0
- package/src/routes/api.auth.$.ts +3 -5
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/auth-registration.test.ts +60 -0
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/auth-registration.ts +40 -0
- package/src/server/billing-email.ts +39 -0
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/__tests__/deliverability.test.ts +68 -0
- package/src/server/email/__tests__/index.test.ts +32 -0
- package/src/server/email/__tests__/outbox.test.tsx +129 -0
- package/src/server/email/__tests__/product-templates.test.tsx +106 -0
- package/src/server/email/__tests__/registry.test.tsx +50 -0
- package/src/server/email/deliverability.ts +57 -0
- package/src/server/email/drivers/console.ts +11 -0
- package/src/server/email/drivers/resend.ts +31 -0
- package/src/server/email/drivers/smtp.ts +27 -0
- package/src/server/email/index.ts +56 -0
- package/src/server/email/outbox.ts +141 -0
- package/src/server/email/preferences.ts +20 -0
- package/src/server/email/registry.ts +38 -0
- package/src/server/email/sender.ts +36 -13
- package/src/server/email/templates/_base/button.tsx +12 -0
- package/src/server/email/templates/_base/email-layout.tsx +117 -0
- package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
- package/src/server/email/templates/_base/theme.ts +22 -0
- package/src/server/email/templates/auth/reset-password.tsx +58 -0
- package/src/server/email/templates/auth/verify-email.tsx +59 -0
- package/src/server/email/templates/auth/welcome.tsx +49 -0
- package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
- package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
- package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
- package/src/server/email/templates/notification/mirror.tsx +50 -0
- package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
- package/src/server/email/templates/product/changelog-release.tsx +65 -0
- package/src/server/email/templates/team/apply-notify.tsx +53 -0
- package/src/server/email/templates/team/approved.tsx +53 -0
- package/src/server/email/templates/team/invite.tsx +57 -0
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +122 -0
- package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
- package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
- package/src/server/notifications/notification-service.ts +71 -1
- package/src/server/notifications/verify-reminder.ts +55 -0
- package/src/server/oauth-config.ts +59 -0
- package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +5 -5
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/auth/email-verification.ts +132 -0
- package/src/server/serverFns/auth/forgot-password.ts +22 -2
- package/src/server/serverFns/auth/providers.ts +4 -3
- package/src/server/serverFns/auth/register.ts +63 -19
- package/src/server/serverFns/auth/registration-mode.ts +23 -0
- package/src/server/serverFns/auth/reset-password.ts +1 -1
- package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
- package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
- package/src/server/serverFns/changelog/subscribe.ts +89 -0
- package/src/server/serverFns/email-unsubscribe.ts +66 -0
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/notifications.ts +10 -0
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +50 -12
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +114 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +105 -4
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /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 [
|
|
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
|
-
|
|
117
|
+
setSelectedUserIds([]);
|
|
112
118
|
setSearchQuery("");
|
|
113
119
|
setMemberRole("member");
|
|
114
120
|
setMemberDialogOpen(true);
|
|
115
121
|
};
|
|
116
122
|
|
|
117
123
|
const handleAddMember = async () => {
|
|
118
|
-
if (
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 ||
|
|
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.
|
|
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.
|
|
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.
|
|
84
|
+
.rejects.toThrow(/非法表名/)
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
it('extraFields values are not concatenated into SQL string', async () => {
|
package/src/lib/app-version.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|