Skip to content

Commit 80cc5bb

Browse files
committed
feat: open organization OAuth scopes + frontend auto-inherit + fix vCard download
- Add organizations:read/write to ALLOWED_SCOPES and OAuth metadata - Frontend handleSaveCard auto-inherits org summary (same as MCP path) - Fix vCard download: replace data: URI with Blob URL (CSP compatible) - Update manifest-loader fallback hashes for new Vite build
1 parent 4f2c9a7 commit 80cc5bb

7 files changed

Lines changed: 65 additions & 12 deletions

File tree

.specify/specs/organization-profiles.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,22 @@ When tools/call save_received_card with organization "台積電" and company_sum
183183
Then card.company_summary remains "自訂摘要" (not overwritten)
184184
```
185185

186-
#### Scenario 5.3: Cron backfill applies org profile to existing cards without summary
186+
#### Scenario 5.3: Frontend save card auto-inherits organization profile
187+
```gherkin
188+
Given organization "台積電" exists with summary "全球最大晶圓代工..."
189+
When frontend handleSaveCard with organization "台積電" and no company_summary
190+
Then card.company_summary is populated from organization profile
191+
And provenance marks it as source_type "inherited"
192+
```
193+
194+
#### Scenario 5.4: Frontend save card with OCR-produced summary does not inherit
195+
```gherkin
196+
Given organization "台積電" exists with summary
197+
When frontend handleSaveCard with organization "台積電" and company_summary "OCR產出摘要"
198+
Then card.company_summary remains "OCR產出摘要" (not overwritten by org profile)
199+
```
200+
201+
#### Scenario 5.5: Cron backfill applies org profile to existing cards without summary
187202
```gherkin
188203
Given 5 cards with organization_normalized matching "台積電"
189204
And 3 of them have NULL company_summary

workers/public/css/tailwind.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workers/public/js/main.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,17 +1307,23 @@ document.getElementById('save-vcard').addEventListener('click', () => {
13071307
if (currentCardData) {
13081308
const vcard = generateVCard(currentCardData);
13091309

1310-
// Safari iOS 不支援 Blob URL 下載,改用 data URI
1311-
const dataUri = 'data:text/vcard;charset=utf-8,' + encodeURIComponent(vcard);
1312-
const a = document.createElement('a');
1313-
a.href = dataUri;
1314-
13151310
// 根據當前語言狀態決定檔名
13161311
const name = typeof currentCardData.name === 'object'
13171312
? (currentLanguage === 'zh' ? (currentCardData.name.zh || currentCardData.name.en) : (currentCardData.name.en || currentCardData.name.zh))
13181313
: currentCardData.name;
1319-
a.download = `${name || 'contact'}.vcf`;
1314+
const filename = `${name || 'contact'}.vcf`;
1315+
1316+
// Blob URL 下載(相容 CSP default-src 'self',現代瀏覽器通用)
1317+
const blob = new Blob([vcard], { type: 'text/vcard;charset=utf-8' });
1318+
const url = URL.createObjectURL(blob);
1319+
const a = document.createElement('a');
1320+
a.href = url;
1321+
a.download = filename;
1322+
a.style.display = 'none';
1323+
document.body.appendChild(a);
13201324
a.click();
1325+
document.body.removeChild(a);
1326+
URL.revokeObjectURL(url);
13211327
showNotification('vCard 已下載', 'success');
13221328
} else {
13231329
showError(currentLanguage === 'zh' ? '無法下載 vCard,請重新載入頁面' : 'Failed to download vCard, please reload the page');

workers/public/js/manifest-loader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
// Fallback hashes — updated by build process or manually
1010
var FALLBACK = {
1111
'icons': 'icons.sS1r72aF.js',
12-
'user-portal': 'user-portal.B9oIbRgM.js',
12+
'user-portal': 'user-portal.IOU4A1v_.js',
1313
'index': 'index.Cl2MLZ2s.js',
1414
'admin-dashboard': 'admin-dashboard.uN8GgcOK.js',
15-
'card-display': 'card-display.Bj1oh34R.js',
15+
'card-display': 'card-display.BlK0n1Xf.js',
1616
'qr-quick': 'qr-quick.HcaXRGQV.js'
1717
};
1818

workers/src/handlers/mcp/oauth-authorize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MCP_AUTH_STATE_PREFIX = 'mcp_auth_state:';
1010
const MCP_AUTH_CODE_PREFIX = 'mcp_auth_code:';
1111
const MCP_AUTH_STATE_TTL = 600; // 10 minutes
1212
const MCP_AUTH_CODE_TTL = 600; // 10 minutes
13-
const ALLOWED_SCOPES = new Set(['received_cards:read', 'received_cards:write']);
13+
const ALLOWED_SCOPES = new Set(['received_cards:read', 'received_cards:write', 'organizations:read', 'organizations:write']);
1414

1515
interface McpAuthState {
1616
client_id: string;

workers/src/handlers/mcp/oauth-metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Env } from '../../types';
22

3-
const SCOPES = ['received_cards:read', 'received_cards:write'];
3+
const SCOPES = ['received_cards:read', 'received_cards:write', 'organizations:read', 'organizations:write'];
44
const CACHE_CONTROL = 'public, max-age=3600';
55

66
/** Use the request's origin so metadata URLs match the domain the client connected to */

workers/src/handlers/user/received-cards/crud.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,38 @@ export async function handleSaveCard(request: Request, env: Env, ctx: ExecutionC
222222
permanentUrl, thumbnailUrl, body.ocr_raw_text || null, now.toString()
223223
).run();
224224

225+
// Auto-inherit org profile summary (if card has no company_summary)
226+
if (body.organization && !body.company_summary) {
227+
try {
228+
const orgProfile = await env.DB.prepare(`
229+
SELECT uuid, summary
230+
FROM organizations
231+
WHERE user_email = ? AND (
232+
name_normalized = ? OR name LIKE ? OR name_en LIKE ?
233+
)
234+
LIMIT 1
235+
`).bind(
236+
user.email, organizationNormalized,
237+
`%${body.organization}%`, `%${body.organization}%`
238+
).first<{ uuid: string; summary: string | null }>();
239+
240+
if (orgProfile?.summary) {
241+
await env.DB.prepare(
242+
`UPDATE received_cards SET company_summary = ? WHERE uuid = ?`
243+
).bind(orgProfile.summary, cardUuid).run();
244+
245+
// Record provenance (non-blocking)
246+
try {
247+
await env.DB.prepare(`
248+
INSERT INTO field_history (entity_type, entity_uuid, field_name,
249+
old_value, new_value, source_type, client_id, user_email, changed_at)
250+
VALUES ('card', ?, 'company_summary', NULL, ?, 'inherited', NULL, ?, ?)
251+
`).bind(cardUuid, orgProfile.summary, user.email, now).run();
252+
} catch { /* provenance failure must not block */ }
253+
}
254+
} catch { /* org lookup failure must not block card save */ }
255+
}
256+
225257
// Auto-extract tags based on organization field
226258
if (body.organization) {
227259
const tags = extractTagsFromOrganization(body.organization);

0 commit comments

Comments
 (0)