Просмотр исходного кода

Merge branch 'master' into feat/158232-prevent-inline-mime-type-sniffing-vulnerabilities

arvid-e 1 месяц назад
Родитель
Сommit
c0c6457ec0

+ 22 - 0
.claude/hooks/session-start.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+set -euo pipefail
+
+# Only run in remote (Claude Code on the web) environments
+if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
+  exit 0
+fi
+
+cd "$CLAUDE_PROJECT_DIR"
+
+# Install all workspace dependencies.
+# turbo (root devDependency) and all workspace packages will be installed.
+pnpm install
+
+# Install turbo globally (mirrors devcontainer postCreateCommand.sh) so it is
+# available as a bare `turbo` command in subsequent Claude tool calls.
+# Falls back to adding node_modules/.bin to PATH if the pnpm global store is
+# not yet configured in this environment.
+if ! command -v turbo &> /dev/null; then
+  pnpm install turbo --global 2>/dev/null \
+    || echo "export PATH=\"$CLAUDE_PROJECT_DIR/node_modules/.bin:\$PATH\"" >> "$CLAUDE_ENV_FILE"
+fi

+ 10 - 0
.claude/settings.json

@@ -1,5 +1,15 @@
 {
   "hooks": {
+    "SessionStart": [
+      {
+        "hooks": [
+          {
+            "type": "command",
+            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
+          }
+        ]
+      }
+    ],
     "PostToolUse": [
       {
         "matcher": "Write|Edit",

+ 1 - 2
AGENTS.md

@@ -116,8 +116,7 @@ Always execute these checks:
 
 ```bash
 # From workspace root (recommended)
-turbo run lint:typecheck --filter @growi/app
-turbo run lint:biome --filter @growi/app
+turbo run lint --filter @growi/app
 turbo run test --filter @growi/app
 turbo run build --filter @growi/app
 ```

+ 0 - 11
apps/app/.claude/skills/app-commands/SKILL.md

@@ -39,17 +39,6 @@ pnpm run lint:styles      # Stylelint only
 
 > **Running individual test files**: See the `testing` rule (`.claude/rules/testing.md`).
 
-### Common Mistake
-
-```bash
-# ❌ WRONG: lint:typecheck is NOT a Turborepo task
-turbo run lint:typecheck --filter @growi/app
-# Error: could not find task `lint:typecheck` in project
-
-# ✅ CORRECT: Use pnpm for package-specific scripts
-pnpm --filter @growi/app run lint:typecheck
-```
-
 ## Quick Reference
 
 | Task | Command |

+ 9 - 0
apps/app/src/features/openai/server/routes/delete-thread.ts

@@ -14,6 +14,7 @@ import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-respo
 import loggerFactory from '~/utils/logger';
 
 import type { IApiv3DeleteThreadParams } from '../../interfaces/thread-relation';
+import ThreadRelationModel from '../models/thread-relation';
 import { getOpenaiService } from '../services/openai';
 import { certifyAiService } from './middlewares/certify-ai-service';
 
@@ -68,6 +69,14 @@ export const deleteThreadFactory = (crowi: Crowi): RequestHandler[] => {
       }
 
       try {
+        const threadRelation = await ThreadRelationModel.findOne({
+          _id: threadRelationId,
+          userId: user._id,
+        });
+        if (threadRelation == null) {
+          return res.apiv3Err(new ErrorV3('Thread not found'), 404);
+        }
+
         const deletedThreadRelation =
           await openaiService.deleteThread(threadRelationId);
         return res.apiv3({ deletedThreadRelation });

+ 1 - 0
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -246,6 +246,7 @@ export const postMessageToEditHandlersFactory = (
 
       const threadRelation = await ThreadRelationModel.findOne({
         threadId: { $eq: threadId },
+        userId: user._id,
       });
       if (threadRelation == null) {
         return res.apiv3Err(new ErrorV3('ThreadRelation not found'), 404);

+ 4 - 2
apps/app/src/features/openai/server/routes/get-threads.ts

@@ -70,8 +70,10 @@ export const getThreadsFactory = (crowi: Crowi): RequestHandler[] => {
           );
         }
 
-        const threads =
-          await openaiService.getThreadsByAiAssistantId(aiAssistantId);
+        const threads = await openaiService.getThreadsByAiAssistantId(
+          aiAssistantId,
+          user._id,
+        );
 
         return res.apiv3({ threads });
       } catch (err) {

+ 9 - 0
apps/app/src/features/openai/server/routes/message/get-messages.ts

@@ -12,6 +12,7 @@ import loginRequiredFactory from '~/server/middlewares/login-required';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
+import ThreadRelationModel from '../../models/thread-relation';
 import { getOpenaiService } from '../../services/openai';
 import { certifyAiService } from '../middlewares/certify-ai-service';
 
@@ -81,6 +82,14 @@ export const getMessagesFactory = (crowi: Crowi): RequestHandler[] => {
           );
         }
 
+        const threadRelation = await ThreadRelationModel.findOne({
+          threadId: { $eq: threadId },
+          userId: user._id,
+        });
+        if (threadRelation == null) {
+          return res.apiv3Err(new ErrorV3('Thread not found'), 404);
+        }
+
         const messages = await openaiService.getMessageData(
           threadId,
           user.lang,

+ 4 - 1
apps/app/src/features/openai/server/routes/message/post-message.ts

@@ -128,7 +128,10 @@ export const postMessageHandlersFactory = (crowi: Crowi): RequestHandler[] => {
         return res.apiv3Err(new ErrorV3('AI assistant not found'), 404);
       }
 
-      const threadRelation = await ThreadRelationModel.findOne({ threadId });
+      const threadRelation = await ThreadRelationModel.findOne({
+        threadId: { $eq: threadId },
+        userId: user._id,
+      });
       if (threadRelation == null) {
         return res.apiv3Err(new ErrorV3('ThreadRelation not found'), 404);
       }

+ 12 - 2
apps/app/src/features/openai/server/services/openai.ts

@@ -98,6 +98,7 @@ export interface IOpenaiService {
   ): Promise<ThreadRelationDocument>;
   getThreadsByAiAssistantId(
     aiAssistantId: string,
+    userId?: string,
   ): Promise<ThreadRelationDocument[]>;
   deleteThread(threadRelationId: string): Promise<ThreadRelationDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
@@ -290,12 +291,21 @@ class OpenaiService implements IOpenaiService {
 
   async getThreadsByAiAssistantId(
     aiAssistantId: string,
+    userId?: string,
     type: ThreadType = ThreadType.KNOWLEDGE,
   ): Promise<ThreadRelationDocument[]> {
-    const threadRelations = await ThreadRelationModel.find({
+    const query: { aiAssistant: string; type: ThreadType; userId?: string } = {
       aiAssistant: aiAssistantId,
       type,
-    }).sort({ updatedAt: -1 });
+    };
+
+    if (userId != null) {
+      query.userId = userId;
+    }
+
+    const threadRelations = await ThreadRelationModel.find(query).sort({
+      updatedAt: -1,
+    });
     return threadRelations;
   }
 

+ 13 - 1
apps/app/src/features/search/client/components/SearchForm.tsx

@@ -41,6 +41,17 @@ export const SearchForm = (props: Props): JSX.Element => {
     [searchKeyword, onSubmit],
   );
 
+  // Prevent Downshift from intercepting Home/End keys so they move
+  // the cursor within the input field instead of navigating the list
+  const keyDownHandler = useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) => {
+      if (e.key === 'Home' || e.key === 'End') {
+        e.nativeEvent.preventDownshiftDefault = true;
+      }
+    },
+    [],
+  );
+
   const inputOptions = useMemo(() => {
     return getInputProps({
       type: 'text',
@@ -49,8 +60,9 @@ export const SearchForm = (props: Props): JSX.Element => {
       ref: inputRef,
       value: searchKeyword,
       onChange: changeSearchTextHandler,
+      onKeyDown: keyDownHandler,
     });
-  }, [getInputProps, searchKeyword, changeSearchTextHandler]);
+  }, [getInputProps, searchKeyword, changeSearchTextHandler, keyDownHandler]);
 
   useEffect(() => {
     if (inputRef.current != null) {

+ 9 - 0
apps/app/src/features/search/client/interfaces/downshift.ts

@@ -1,5 +1,14 @@
 import type { ControllerStateAndHelpers } from 'downshift';
 
+// Augment the global Event interface with downshift's custom property.
+// downshift checks event.nativeEvent.preventDownshiftDefault to skip
+// its default key handling. See: https://www.downshift-js.com/downshift#customizing-handlers
+declare global {
+  interface Event {
+    preventDownshiftDefault?: boolean;
+  }
+}
+
 export type DownshiftItem = { url: string };
 
 export type GetItemProps =