Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ const MainCard = () => {
// api 구간
const {mutate:postSignData} = usePostSignUp();

// 익스텐션에서부터 이메일 받아오는 구간!
const [userEmail, setUserEmail] = useState("");

useEffect(() => {
const params = new URLSearchParams(location.search);
const emailParam = params.get("email");
if (emailParam) {
setUserEmail(emailParam);
}
}, [location.search]);


// FCM 구간
const [fcmToken, setFcmToken] = useState<string | null>(null);
const app = initializeApp(firebaseConfig);
Expand Down Expand Up @@ -126,7 +138,7 @@ const [remindTime, setRemindTime] = useState('09:00');
setRemindTime(normalizeTime(raw));

postSignData({
"email": "tesdfdfsst@gmail.com", // TODO : 익스텐션에게서 메일 받기
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
Comment on lines 140 to 144
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 누락 시 가입 요청 차단

userEmail이 비어있으면 요청을 막고 사용자 안내하세요.

-      postSignData({
+      if (!userEmail) {
+        alert('이메일 정보가 없어 회원가입을 진행할 수 없어요.');
+        return;
+      }
+      postSignData({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
postSignData({
"email": "tesdfdfsst@gmail.com", // TODO : 익스텐션에게서 메일 받기
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
if (!userEmail) {
alert('이메일 정보가 없어 회원가입을 진행할 수 없어요.');
return;
}
postSignData({
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
140-144, the signup call is made even when userEmail is empty; add a guard that
validates userEmail (e.g., check trimmed length) before calling postSignData,
and if it's missing return early and show the user an inline error or toast
message indicating the email is required; ensure the function exits without
making the request when validation fails.

Expand Down
11 changes: 10 additions & 1 deletion apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,18 @@ export const usePostSignUp = () => {
mutationFn: (data: postSignUpRequest) => postSignUp(data),
onSuccess: (data) => {
const newToken = data?.data?.token || data?.token;

const sendTokenToExtension = (token: string) => {
window.postMessage(
{
type: 'SET_TOKEN',
token,
},
window.location.origin
);
};
if (newToken) {
localStorage.setItem('token', newToken);
sendTokenToExtension(newToken);
}
Comment on lines 73 to 76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

토큰을 localStorage에 저장하는 것은 XSS에 취약

가능하다면 서버가 HttpOnly Secure 쿠키로 토큰(또는 세션)을 설정하도록 전환하세요. 프론트에서는 localStorage 보관을 제거합니다.

대안:

  • 서버: /auth/signup 응답 시 Set-Cookie(HttpOnly; Secure; SameSite=Strict; Path=/)로 액세스/리프레시 토큰 설정.
  • 클라이언트: Authorization 헤더 제거, 쿠키 기반 인증으로 전환.
  • 불가 시: 토큰을 메모리 저장 + 짧은 만료 + 엄격한 CSP로 리스크 완화.
🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 73-73: Detected potential storage of sensitive information in browser localStorage. Sensitive data like email addresses, personal information, or authentication tokens should not be stored in localStorage as it's accessible to any script.
Context: localStorage.setItem('token', newToken)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://owasp.org/www-community/vulnerabilities/HTML5_Security_Cheat_Sheet
- https://cwe.mitre.org/data/definitions/312.html

(browser-storage-sensitive-data)


console.log('회원가입 성공:', data);
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"icons": {
"128": "icon.png"
},
"permissions": ["activeTab", "tabs", "storage", "scripting", "bookmarks"],
"permissions": ["activeTab", "tabs", "storage", "bookmarks", "identity","identity.email"],
"background": {
"service_worker": "src/background.js",
"type": "module"
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const App = () => {
};

const handleDuplicateRightClick = () => {
window.location.href = "/dashboard";
window.location.href = "https://www.pinback.today/";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

외부 페이지 이동은 chrome.tabs.create를 사용하세요

popup 내부에서 window.location.href 변경은 UX가 어색합니다. 새 탭을 여는 쪽이 일반적이며, 환경별 베이스 URL로 관리하세요.

-  const handleDuplicateRightClick = () => {
-    window.location.href = "https://www.pinback.today/";
-  };
+  const handleDuplicateRightClick = () => {
+    const url = import.meta.env.VITE_WEB_BASE_URL ?? "https://www.pinback.today/";
+    chrome.tabs.create({ url });
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.location.href = "https://www.pinback.today/";
const handleDuplicateRightClick = () => {
const url = import.meta.env.VITE_WEB_BASE_URL ?? "https://www.pinback.today/";
chrome.tabs.create({ url });
};
🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around line 27, replace the direct
window.location.href assignment with chrome.tabs.create to open
"https://www.pinback.today/" in a new tab using a base URL taken from your
environment/config (do not hardcode); after creating the tab, close the popup
(window.close()) to match extension UX. Also ensure the extension has the "tabs"
permission or use chrome.tab API appropriate to your manifest version and pull
the base URL from a central config/env var so it can vary by environment.

};

return (
Expand Down
6 changes: 2 additions & 4 deletions apps/extension/src/apis/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
);
const newToken = response.data.data.token;
chrome.storage.local.set({ token: newToken }, () => {
console.log('Token re-saved to chrome storage');

Check warning on line 19 in apps/extension/src/apis/axiosInstance.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
});
return newToken;
};
Expand All @@ -28,14 +28,12 @@
if (isNoAuth) return config;

const email = await new Promise<string | undefined>((resolve) => {
chrome.storage.local.get('email', (result) => {
resolve(result.email);
});
chrome.storage.local.get('userEmail', (result) => resolve(result.userEmail));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storage이거 key email로 되어있지 않나요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넹네! 이거 익스텐션 크롬스토리지랑 그냥 웹 로컬스토리지 각각의 키 구분할라고 (제가 헷갈려서..ㅎ)
익스텐션 쪽은 userEmail로 했씁니당!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 익스텐션 axios interceptor 확인해보셔야 할 것 같아요! 이전에 인터셉터 설정할때 email로 통일한 기억이..!

});

let token = await new Promise<string | undefined>((resolve) => {
chrome.storage.local.get('token', (result) => {
resolve(result.token);
resolve(result.token);
});
});

Expand Down
55 changes: 20 additions & 35 deletions apps/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,24 @@
console.log('백그라운드 기능');

Check warning on line 1 in apps/extension/src/background.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'FETCH_OG_META') {
fetch(message.url)
.then((res) => res.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

const getMeta = (prop) =>
doc
.querySelector(`meta[property="${prop}"]`)
?.getAttribute('content') || '';

const makeAbsoluteUrl = (base, img) => {
try {
return img ? new URL(img, base).href : '';
} catch {
return img;
}
};

const image = getMeta('og:image');

sendResponse({
title: getMeta('og:title'),
description: getMeta('og:description'),
siteName: getMeta('og:site_name'),
image: makeAbsoluteUrl(message.url, image),
url: getMeta('og:url') || message.url,
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
chrome.identity.getProfileUserInfo(function (info) {
chrome.storage.local.set({ 'userEmail': info.email }, () => {
console.log(info.email);

Check warning on line 6 in apps/extension/src/background.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
});
Comment on lines +4 to 7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

이메일 수집 시 PII 로그 제거 + 빈 이메일/에러 처리 필요

  • 사용자 이메일을 콘솔에 출력하면 PII 노출 위험이 큽니다.
  • 크롬에 로그인되어 있지 않으면 info.email이 빈 문자열일 수 있어 가드가 필요합니다.
  • chrome.storage.local.setruntime.lastError 체크가 없습니다.

아래처럼 수정해 주세요.

-    chrome.identity.getProfileUserInfo(function (info) {
-       chrome.storage.local.set({ 'userEmail': info.email }, () => {
-          console.log(info.email);
-        });
+    chrome.identity.getProfileUserInfo(function (info) {
+      const email = info?.email?.trim();
+      if (email) {
+        chrome.storage.local.set({ userEmail: email }, () => {
+          if (chrome.runtime.lastError) {
+            console.warn('userEmail 저장 실패:', chrome.runtime.lastError.message);
+          }
+        });
+      } else {
+        console.warn('Chrome 프로필 이메일을 사용할 수 없습니다(비로그인/권한 미부여).');
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chrome.identity.getProfileUserInfo(function (info) {
chrome.storage.local.set({ 'userEmail': info.email }, () => {
console.log(info.email);
});
chrome.identity.getProfileUserInfo(function (info) {
const email = info?.email?.trim();
if (email) {
chrome.storage.local.set({ email }, () => {
if (chrome.runtime.lastError) {
console.warn('email 저장 실패:', chrome.runtime.lastError.message);
}
});
} else {
console.warn('Chrome 프로필 이메일을 사용할 수 없습니다(비로그인/권한 미부여).');
}
});
🧰 Tools
🪛 GitHub Check: lint

[warning] 6-6:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 4-7, remove the console.log
that prints info.email to avoid PII exposure; add a guard that checks if
info.email is a non-empty string before calling chrome.storage.local.set (skip
storing if empty and handle accordingly); after calling
chrome.storage.local.set, check chrome.runtime.lastError and handle/log it via
chrome.runtime.lastError.message (do not log the email itself); ensure any error
or empty-email paths are handled gracefully (e.g., set a flag like
userEmailAvailable: false or return early).

})
.catch((err) => {
console.error('OG fetch 실패:', err);
sendResponse(null);
});
return true; // async 응답
setTimeout(() => {
chrome.tabs.create({
url: `http://localhost:5173/onboarding?email=${info.email}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

개발용 localhost URL 하드코딩 제거

배포 시 깨집니다. 환경변수/설정으로 분리하세요.

적용 예(파일 상단 등 외부 추가 코드):

// 외부 추가 코드
const ONBOARDING_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://app.pinback.io';
const ONBOARDING_URL = new URL('/onboarding', ONBOARDING_ORIGIN).toString();

해당 라인 교체:

-          url: `http://localhost:5173/onboarding?email=${info.email}`,
+          url: ONBOARDING_URL,
🤖 Prompt for AI Agents
In apps/extension/src/background.ts around line 10, the onboarding URL is
hardcoded to localhost which will break in production; define an
environment-backed origin constant at the top of the file (e.g., read
import.meta.env.VITE_CLIENT_ORIGIN with a production fallback like
https://app.pinback.io), build the onboarding base URL using that origin and the
/onboarding path, then replace the hardcoded string with code that appends the
email query param to that constructed URL so runtime origin is configurable via
env.

});
}, 1000);
Comment on lines +8 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

이메일을 URL 쿼리로 전달하지 마세요(PII 유출 위험) + 불필요한 setTimeout 제거

  • 이메일을 쿼리스트링으로 넘기면 서버 로그/리퍼러/분석 도구로 쉽게 유출될 수 있습니다.
  • 1초 대기는 비결정적입니다. 탭 생성 콜백에서 바로 처리하세요.
  • 권장: 탭만 열고, 컨텐츠 스크립트로 이메일을 전달해 페이지로 postMessage 브리징.
-      setTimeout(() => {
-        chrome.tabs.create({
-          url: `http://localhost:5173/onboarding?email=${info.email}`,
-        });
-      }, 1000);
+      chrome.tabs.create({ url: 'http://localhost:5173/onboarding' }, (tab) => {
+        const email = info?.email?.trim();
+        if (!email || !tab?.id) return;
+        // 컨텐츠 스크립트가 페이지로 window.postMessage('SET_EMAIL') 포워딩
+        chrome.tabs.sendMessage(
+          tab.id,
+          { type: 'SET_EMAIL', email },
+          () => void chrome.runtime.lastError // 컨텐츠 스크립트 로드 전 호출시 에러 무시
+        );
+      });

컨텐츠 스크립트 측에서는 SET_EMAIL 수신 → window.postMessage로 페이지에 전달하도록 맞춰 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => {
chrome.tabs.create({
url: `http://localhost:5173/onboarding?email=${info.email}`,
});
}, 1000);
chrome.tabs.create({ url: 'http://localhost:5173/onboarding' }, (tab) => {
const email = info?.email?.trim();
if (!email || !tab?.id) return;
// 컨텐츠 스크립트가 페이지로 window.postMessage('SET_EMAIL') 포워딩
chrome.tabs.sendMessage(
tab.id,
{ type: 'SET_EMAIL', email },
() => void chrome.runtime.lastError
);
});

});
}
});


chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'SET_TOKEN') {
chrome.storage.local.set({ 'token': message.token }, () => {
console.log('Token saved!', message.token);

Check warning on line 21 in apps/extension/src/background.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
});
}
});
Comment on lines +18 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

토큰 콘솔 출력 제거 + 검증/응답 처리 + 발신자 검증 추가

  • 토큰을 콘솔에 남기면 심각한 보안 리스크입니다.
  • 메시지 스키마 검증과 저장 성공/실패 응답을 추가해 신뢰성을 높이세요.
  • 내부 발신자만 허용하도록 sender.id를 점검하세요.
-chrome.runtime.onMessage.addListener((message) => {
-  if (message.type === 'SET_TOKEN') {
-    chrome.storage.local.set({ 'token': message.token }, () => {
-      console.log('Token saved!', message.token);
-    });
-  }
-});
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message?.type !== 'SET_TOKEN') return;
+  if (sender?.id && sender.id !== chrome.runtime.id) {
+    sendResponse({ ok: false, error: 'unauthorized_sender' });
+    return;
+  }
+  const token = typeof message.token === 'string' ? message.token.trim() : '';
+  if (!token) {
+    sendResponse({ ok: false, error: 'invalid_token' });
+    return;
+  }
+  chrome.storage.local.set({ token }, () => {
+    if (chrome.runtime.lastError) {
+      sendResponse({ ok: false, error: chrome.runtime.lastError.message });
+    } else {
+      sendResponse({ ok: true });
+    }
+  });
+  return true; // 비동기 응답 유지
+});

참고: 이슈 #85 요구사항에 따라 refreshToken도 함께 저장해야 하면 동일 스키마로 확장하세요.

🧰 Tools
🪛 GitHub Check: lint

[warning] 21-21:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 18-24, remove the console.log
that prints the token, validate the incoming message schema (ensure message.type
=== 'SET_TOKEN' and required fields like token and optional refreshToken are
strings), verify the sender by checking sender?.id === chrome.runtime.id before
proceeding, persist token (and refreshToken if present) via
chrome.storage.local.set and in its callback send a success or failure response
back to the sender (including an error message on failure), and ensure any
exceptions are caught and result in a failure response; expand schema and
storage to include refreshToken if Issue #85 requires it.

9 changes: 9 additions & 0 deletions apps/extension/src/content.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
console.log('컨텐츠 스크립트 로드됨');

Check warning on line 1 in apps/extension/src/content.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'SET_TOKEN') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
});
Comment on lines +2 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

postMessage 출처 검증 없음 → 임의 사이트가 토큰 주입 가능

콘텐츠 스크립트가 모든 사이트(<all_urls>)에서 메시지를 수신합니다. origin 화이트리스트 및 페이로드 검증이 필요합니다.

-window.addEventListener('message', (event) => {
+const ALLOWED_ORIGINS = ['https://www.pinback.today', 'http://localhost:5173'];
+window.addEventListener('message', (event) => {
   if (event.source !== window) return;
-  if (event.data.type === 'SET_TOKEN') {
+  if (!ALLOWED_ORIGINS.includes(event.origin)) return;
+  if (event.data?.type === 'SET_TOKEN' && typeof event.data.token === 'string') {
     chrome.runtime.sendMessage({
       type: 'SET_TOKEN',
       token: event.data.token,
     });
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'SET_TOKEN') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
});
const ALLOWED_ORIGINS = ['https://www.pinback.today', 'http://localhost:5173'];
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (!ALLOWED_ORIGINS.includes(event.origin)) return;
if (event.data?.type === 'SET_TOKEN' && typeof event.data.token === 'string') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
});
🤖 Prompt for AI Agents
In apps/extension/src/content.ts around lines 2 to 10, the window message
handler accepts messages from any origin and blindly forwards tokens to the
extension; add origin and payload validation: define a whitelist array of
allowed origins and immediately return unless event.origin is in that whitelist,
verify event.source === window (keep) and that event.data is an object with type
=== 'SET_TOKEN' and that event.data.token is a non-empty string (optionally
validate format/length), then only forward the sanitized token to
chrome.runtime.sendMessage; also drop or log unexpected messages and avoid
forwarding if validation fails.

3 changes: 1 addition & 2 deletions apps/extension/src/hooks/useSaveBookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
createdAt: new Date().toISOString(),
};

const result = await new Promise<{ bookmarks?: any[] }>((resolve) => {

Check warning on line 21 in apps/extension/src/hooks/useSaveBookmarks.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
chrome.storage.local.get(['bookmarks'], (items) => resolve(items));
});

Expand All @@ -36,13 +36,12 @@
url: params.url,
},
(newBookmark) => {
console.log('크롬 북마크바에 저장 완료: ', newBookmark);

Check warning on line 39 in apps/extension/src/hooks/useSaveBookmarks.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
}
);

// TODO: window.close();
window.close();
} catch (error) {
console.error('저장 중 오류:', error);

Check warning on line 44 in apps/extension/src/hooks/useSaveBookmarks.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
}
};

Expand Down
5 changes: 2 additions & 3 deletions apps/extension/src/pages/MainPop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@

useEffect(() => {
if (!loading && !title) {
// TODO 개발 중에는 주석처리 (최종엔 주석 제거할거임)
// alert("이 페이지는 저장할 수 없어요 🐿️");
// window.close();
alert("이 페이지는 저장할 수 없어요 🐿️");
window.close();
}
}, [loading, title]);

Expand Down Expand Up @@ -128,7 +127,7 @@
setDate(newDate);
setTime(newTime);
}
}, [remindData]);

Check warning on line 130 in apps/extension/src/pages/MainPop.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'type'. Either include it or remove the dependency array
const [dateError, setDateError] = useState('');
const [timeError, setTimeError] = useState('');

Expand Down
Loading