ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

refactor extension to require authentication and use proper upload flow

Removed temporary storage approach and implemented proper authentication flow:

Extension changes:
- Added session check to popup init flow (checkSession in api-client)
- Added "not logged in" state with login prompts
- Updated uploadToATlast to include credentials for cookie-based auth
- Extension now requires user to be logged in BEFORE scanning

Backend changes:
- Converted extension-import to AuthenticatedHandler (requires auth)
- Now creates upload records immediately (no temporary storage)
- Removed extension_imports table from database schema
- Deleted get-extension-import function (no longer needed)
- Deleted import-store utility (temporary approach removed)

Frontend changes:
- Removed ExtensionImport page and /import/:id route
- Extension uploads now use same flow as file uploads

This matches the correct user flow: user logs in to ATlast first, then
extension creates permanent upload records directly (same as file uploads).

Built extension successfully for dev environment.

byarielm.fyi b00ecad7 69db377f

verified
+143
EXTENSION_STATUS.md
··· 1 + # Extension Implementation Status 2 + 3 + ## Current State: READY FOR TESTING ⚠️ 4 + 5 + ### What's Complete ✅ 6 + 7 + 1. **Environment Configuration** 8 + - Dev/prod builds with correct API URLs 9 + - Build: `npm run build` (dev) or `npm run build:prod` 10 + - Dev: `http://127.0.0.1:8888` 11 + - Prod: `https://atlast.byarielm.fyi` 12 + 13 + 2. **Server Health Check** 14 + - Extension checks if dev server is running (dev mode only) 15 + - Shows "Server offline" state with instructions 16 + - "Check Again" button to retry 17 + 18 + 3. **Authentication Flow** 19 + - Extension checks `/session` endpoint on init 20 + - Shows "Not logged in" state if no session 21 + - "Open ATlast" button to log in 22 + - "Check Again" to retry after login 23 + - **User must be logged in to ATlast BEFORE using extension** 24 + 25 + 4. **Upload Flow** (matches file upload) 26 + - Scan Twitter Following page 27 + - POST to `/extension-import` (requires auth) 28 + - Backend: 29 + - Gets DID from session 30 + - Creates `user_upload` entry 31 + - Saves to `source_accounts` table 32 + - Returns `uploadId` 33 + - Opens `/results?uploadId={id}` 34 + - Frontend searches and displays (same as file upload) 35 + 36 + 5. **CORS Permissions** 37 + - Extension has host_permissions for: 38 + - `http://127.0.0.1:8888/*` 39 + - `http://localhost:8888/*` 40 + - `https://atlast.byarielm.fyi/*` 41 + 42 + 6. **Cleanup Complete** 43 + - ❌ Removed `extension_imports` table 44 + - ❌ Removed `get-extension-import` function 45 + - ❌ Removed `ExtensionImport.tsx` page 46 + - ❌ Removed `/import/:id` route 47 + - ❌ Removed `utils/import-store.ts` 48 + 49 + ### What Needs Testing 🧪 50 + 51 + 1. **Full Flow Test** 52 + ```bash 53 + # 1. Start dev server 54 + npx netlify-cli dev --filter @atlast/web 55 + 56 + # 2. Build extension 57 + cd packages/extension 58 + npm run build 59 + 60 + # 3. Load extension in Chrome 61 + chrome://extensions/ → Load unpacked → packages/extension/dist/chrome/ 62 + 63 + # 4. Log in to ATlast 64 + Open http://127.0.0.1:8888 → Log in 65 + 66 + # 5. Go to Twitter 67 + https://twitter.com/justadev_atlast/following 68 + 69 + # 6. Open extension popup 70 + - Should show "Ready to scan Twitter/X" 71 + - Click "Start Scan" 72 + - Wait for completion 73 + - Click "Open in ATlast" 74 + - Should open /results?uploadId={id} 75 + - Results should load and search automatically 76 + ``` 77 + 78 + 2. **Error Cases to Test** 79 + - Not logged in → should show login prompt 80 + - Server offline → should show offline state 81 + - Empty results → should show appropriate message 82 + - Network errors → should handle gracefully 83 + 84 + ### Current Issues 🐛 85 + 86 + None known - ready for testing! 87 + 88 + ### Next Steps 📋 89 + 90 + 1. ✅ Build extension: `cd packages/extension && npm run build` 91 + 2. ✅ Reload extension in Chrome 92 + 3. ✅ Test login flow 93 + 4. ✅ Test scan and upload 94 + 5. ✅ Verify results page works 95 + 6. Fix any bugs found 96 + 7. Test production build: `npm run build:prod` 97 + 98 + ### Architecture Notes 📝 99 + 100 + **Removed temporary import storage approach:** 101 + - Previously tried in-memory storage (doesn't work in serverless) 102 + - Then tried database storage with temp table (overkill) 103 + 104 + **Current approach:** 105 + - User logs in to ATlast FIRST 106 + - Extension requires authentication 107 + - Upload creates permanent records immediately 108 + - No temporary storage needed 109 + - Matches file upload behavior exactly 110 + 111 + **Why this is better:** 112 + - Simpler architecture 113 + - No temporary storage to expire 114 + - Proper user association from the start 115 + - Reuses existing upload/search infrastructure 116 + - Same flow as file uploads (consistency) 117 + 118 + ### Files Modified in Latest Refactor 119 + 120 + **Deleted:** 121 + - `packages/functions/src/get-extension-import.ts` 122 + - `packages/functions/src/utils/import-store.ts` 123 + - `packages/web/src/pages/ExtensionImport.tsx` 124 + 125 + **Modified:** 126 + - `packages/functions/src/extension-import.ts` - Now requires auth, creates upload 127 + - `packages/functions/src/infrastructure/database/DatabaseService.ts` - Removed extension_imports table 128 + - `packages/functions/src/core/types/database.types.ts` - Removed ExtensionImportRow 129 + - `packages/web/src/Router.tsx` - Removed /import/:id route 130 + - `packages/extension/src/popup/popup.ts` - Added session check, login state 131 + - `packages/extension/src/popup/popup.html` - Added not-logged-in state 132 + - `packages/extension/src/lib/api-client.ts` - Added checkSession(), credentials: 'include' 133 + 134 + ### Decision Graph Summary 135 + 136 + **Total nodes:** 284 137 + **Key decisions tracked:** 138 + - Environment configuration approach (#261-269) 139 + - Port 8888 conflict resolution (#270-274) 140 + - CORS permissions fix (#275-277) 141 + - Storage approach: in-memory → database → proper auth flow (#278-284) 142 + 143 + **Live graph:** https://notactuallytreyanastasio.github.io/deciduous/
+24 -16
docs/git-history.json
··· 1 1 [ 2 2 { 3 - "hash": "90067b8feb9f40512b9732a05b1a8756f338a481", 4 - "short_hash": "90067b8", 3 + "hash": "d0bcf337b6d223a86443f6f67767e87b74e4dd7d", 4 + "short_hash": "d0bcf33", 5 5 "author": "Ariel M. Lighty", 6 - "date": "2025-12-25T21:43:51-05:00", 7 - "message": "add dev server health check and offline detection to extension\n\nFeatures:\n- Check server health on popup init (dev mode only)\n- Show 'server offline' state with setup instructions\n- 'Check Again' button to retry connection\n- Display target server URL for debugging\n- 3-second timeout for health checks\n\nFixes port 8888 conflict workflow - extension now prompts user\nto start dev server instead of hanging silently.", 8 - "files_changed": 4 6 + "date": "2025-12-26T00:26:09-05:00", 7 + "message": "refactor extension to require authentication and use proper upload flow\n\nRemoved temporary storage approach and implemented proper authentication flow:\n\nExtension changes:\n- Added session check to popup init flow (checkSession in api-client)\n- Added \"not logged in\" state with login prompts\n- Updated uploadToATlast to include credentials for cookie-based auth\n- Extension now requires user to be logged in BEFORE scanning\n\nBackend changes:\n- Converted extension-import to AuthenticatedHandler (requires auth)\n- Now creates upload records immediately (no temporary storage)\n- Removed extension_imports table from database schema\n- Deleted get-extension-import function (no longer needed)\n- Deleted import-store utility (temporary approach removed)\n\nFrontend changes:\n- Removed ExtensionImport page and /import/:id route\n- Extension uploads now use same flow as file uploads\n\nThis matches the correct user flow: user logs in to ATlast first, then\nextension creates permanent upload records directly (same as file uploads).\n\nBuilt extension successfully for dev environment.", 8 + "files_changed": 12 9 9 }, 10 10 { 11 - "hash": "4d1480611a83f82da7bb2fe4c89f9f2180ecfa7b", 12 - "short_hash": "4d14806", 11 + "hash": "69db377f7f6dd4e38677f2507ad660cdf1a66465", 12 + "short_hash": "69db377", 13 13 "author": "Ariel M. Lighty", 14 - "date": "2025-12-25T20:58:38-05:00", 15 - "message": "add environment-based build configuration for extension\n\n- Add dev vs prod build modes using --prod flag\n- Inject API URL at build time via esbuild define\n- Dev: http://127.0.0.1:8888\n- Prod: https://atlast.byarielm.fyi\n- Add build:prod and package:prod scripts", 16 - "files_changed": 3 14 + "date": "2025-12-25T22:00:53-05:00", 15 + "message": "update decision graph", 16 + "files_changed": 2 17 + }, 18 + { 19 + "hash": "917687b455a241413e60086b751902156352e098", 20 + "short_hash": "917687b", 21 + "author": "Ariel M. Lighty", 22 + "date": "2025-12-25T20:59:22-05:00", 23 + "message": "update decision graph", 24 + "files_changed": 2 17 25 }, 18 26 { 19 27 "hash": "c35fb0d83202607facc203dfe10325e8672ea67e", ··· 32 40 "files_changed": 3 33 41 }, 34 42 { 35 - "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 36 - "short_hash": "32cdee3", 43 + "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 44 + "short_hash": "ba29fd6", 37 45 "author": "Ariel M. Lighty", 38 46 "date": "2025-12-25T13:22:32-05:00", 39 47 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 40 - "files_changed": 4 48 + "files_changed": 5 41 49 }, 42 50 { 43 - "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 44 - "short_hash": "ba29fd6", 51 + "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 52 + "short_hash": "32cdee3", 45 53 "author": "Ariel M. Lighty", 46 54 "date": "2025-12-25T13:22:32-05:00", 47 55 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 48 - "files_changed": 5 56 + "files_changed": 4 49 57 }, 50 58 { 51 59 "hash": "c3e7afad396d130791d801a85cbfc9643bcd6309",
+187
docs/graph-data.json
··· 3046 3046 "created_at": "2025-12-25T21:59:27.214048800-05:00", 3047 3047 "updated_at": "2025-12-25T21:59:41.037717800-05:00", 3048 3048 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3049 + }, 3050 + { 3051 + "id": 278, 3052 + "change_id": "fa11e7d7-ac30-4d0e-bc8a-d2332f724d92", 3053 + "node_type": "goal", 3054 + "title": "Store extension imports in database and integrate with existing upload flow", 3055 + "description": null, 3056 + "status": "completed", 3057 + "created_at": "2025-12-25T22:05:53.102585900-05:00", 3058 + "updated_at": "2025-12-25T22:20:26.309175100-05:00", 3059 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90,\"prompt\":\"Import Error: Import not found or expired. Please try scanning again. - received after \\\"Open in ATlast\\\". We should be storing this in the existing DB schema with a \\\"save to ATlast\\\" button that also navigates to the results for that upload in atlast.\"}" 3060 + }, 3061 + { 3062 + "id": 279, 3063 + "change_id": "0710252d-bcf6-4708-b67f-d9615a0dad6e", 3064 + "node_type": "observation", 3065 + "title": "In-memory store doesn't work in serverless - each Netlify function can run in different process. Need database storage.", 3066 + "description": null, 3067 + "status": "completed", 3068 + "created_at": "2025-12-25T22:06:43.366406500-05:00", 3069 + "updated_at": "2025-12-25T22:20:26.419201900-05:00", 3070 + "metadata_json": "{\"branch\":\"master\",\"confidence\":100}" 3071 + }, 3072 + { 3073 + "id": 280, 3074 + "change_id": "f38929d4-0ad0-43ec-a25c-a9dd6f9ee7fd", 3075 + "node_type": "action", 3076 + "title": "Create extension_imports table and update endpoints to use database", 3077 + "description": null, 3078 + "status": "completed", 3079 + "created_at": "2025-12-25T22:06:46.169277300-05:00", 3080 + "updated_at": "2025-12-25T22:20:26.516060800-05:00", 3081 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90}" 3082 + }, 3083 + { 3084 + "id": 281, 3085 + "change_id": "0e917ade-9f83-4246-9a66-1aa2dfef7c41", 3086 + "node_type": "outcome", 3087 + "title": "Replaced in-memory storage with database persistence for extension imports", 3088 + "description": null, 3089 + "status": "completed", 3090 + "created_at": "2025-12-25T22:20:19.197297700-05:00", 3091 + "updated_at": "2025-12-25T22:20:26.608871700-05:00", 3092 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3093 + }, 3094 + { 3095 + "id": 282, 3096 + "change_id": "206347b5-4178-43dd-bb05-657b3788a6b0", 3097 + "node_type": "action", 3098 + "title": "Refactoring to proper flow: check session, save immediately, match real upload behavior", 3099 + "description": null, 3100 + "status": "completed", 3101 + "created_at": "2025-12-26T00:00:13.136356300-05:00", 3102 + "updated_at": "2025-12-26T00:19:11.083067300-05:00", 3103 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90}" 3104 + }, 3105 + { 3106 + "id": 283, 3107 + "change_id": "e3adddaf-9126-4bfa-8d75-aa8b94323077", 3108 + "node_type": "observation", 3109 + "title": "Extension-import now requires auth, creates upload immediately, saves to source_accounts - matches file upload flow", 3110 + "description": null, 3111 + "status": "completed", 3112 + "created_at": "2025-12-26T00:13:01.938755900-05:00", 3113 + "updated_at": "2025-12-26T00:19:11.191827900-05:00", 3114 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3115 + }, 3116 + { 3117 + "id": 284, 3118 + "change_id": "3d9caa98-6f9c-4613-9c05-92566f9ee0d5", 3119 + "node_type": "outcome", 3120 + "title": "Refactored extension flow: requires login first, creates upload immediately, matches file upload behavior", 3121 + "description": null, 3122 + "status": "completed", 3123 + "created_at": "2025-12-26T00:18:53.900318900-05:00", 3124 + "updated_at": "2025-12-26T00:19:11.322802200-05:00", 3125 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3126 + }, 3127 + { 3128 + "id": 285, 3129 + "change_id": "f0da412f-562b-4e45-b83d-eba28fc22eea", 3130 + "node_type": "outcome", 3131 + "title": "Extension built successfully for dev environment", 3132 + "description": null, 3133 + "status": "pending", 3134 + "created_at": "2025-12-26T00:24:02.307648100-05:00", 3135 + "updated_at": "2025-12-26T00:24:02.307648100-05:00", 3136 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95,\"files\":[\"packages/extension/build.js\",\"packages/extension/dist/\"]}" 3137 + }, 3138 + { 3139 + "id": 286, 3140 + "change_id": "60c9ec75-7e3f-4aa4-b8cf-0691ef92d260", 3141 + "node_type": "outcome", 3142 + "title": "Committed extension refactor with decision graph", 3143 + "description": null, 3144 + "status": "pending", 3145 + "created_at": "2025-12-26T00:26:17.378515100-05:00", 3146 + "updated_at": "2025-12-26T00:26:17.378515100-05:00", 3147 + "metadata_json": "{\"branch\":\"master\",\"commit\":\"d0bcf33\",\"confidence\":100}" 3049 3148 } 3050 3149 ], 3051 3150 "edges": [ ··· 5974 6073 "weight": 1.0, 5975 6074 "rationale": "Fix complete", 5976 6075 "created_at": "2025-12-25T21:59:40.544320600-05:00" 6076 + }, 6077 + { 6078 + "id": 267, 6079 + "from_node_id": 278, 6080 + "to_node_id": 279, 6081 + "from_change_id": "fa11e7d7-ac30-4d0e-bc8a-d2332f724d92", 6082 + "to_change_id": "0710252d-bcf6-4708-b67f-d9615a0dad6e", 6083 + "edge_type": "leads_to", 6084 + "weight": 1.0, 6085 + "rationale": "Root cause identified", 6086 + "created_at": "2025-12-25T22:07:08.406207600-05:00" 6087 + }, 6088 + { 6089 + "id": 268, 6090 + "from_node_id": 279, 6091 + "to_node_id": 280, 6092 + "from_change_id": "0710252d-bcf6-4708-b67f-d9615a0dad6e", 6093 + "to_change_id": "f38929d4-0ad0-43ec-a25c-a9dd6f9ee7fd", 6094 + "edge_type": "leads_to", 6095 + "weight": 1.0, 6096 + "rationale": "Action to fix", 6097 + "created_at": "2025-12-25T22:07:08.525762700-05:00" 6098 + }, 6099 + { 6100 + "id": 269, 6101 + "from_node_id": 280, 6102 + "to_node_id": 281, 6103 + "from_change_id": "f38929d4-0ad0-43ec-a25c-a9dd6f9ee7fd", 6104 + "to_change_id": "0e917ade-9f83-4246-9a66-1aa2dfef7c41", 6105 + "edge_type": "leads_to", 6106 + "weight": 1.0, 6107 + "rationale": "Implementation complete", 6108 + "created_at": "2025-12-25T22:20:23.139445800-05:00" 6109 + }, 6110 + { 6111 + "id": 270, 6112 + "from_node_id": 278, 6113 + "to_node_id": 281, 6114 + "from_change_id": "fa11e7d7-ac30-4d0e-bc8a-d2332f724d92", 6115 + "to_change_id": "0e917ade-9f83-4246-9a66-1aa2dfef7c41", 6116 + "edge_type": "leads_to", 6117 + "weight": 1.0, 6118 + "rationale": "Goal achieved", 6119 + "created_at": "2025-12-25T22:20:23.242682400-05:00" 6120 + }, 6121 + { 6122 + "id": 271, 6123 + "from_node_id": 282, 6124 + "to_node_id": 284, 6125 + "from_change_id": "206347b5-4178-43dd-bb05-657b3788a6b0", 6126 + "to_change_id": "3d9caa98-6f9c-4613-9c05-92566f9ee0d5", 6127 + "edge_type": "leads_to", 6128 + "weight": 1.0, 6129 + "rationale": "Refactor complete", 6130 + "created_at": "2025-12-26T00:18:57.785597400-05:00" 6131 + }, 6132 + { 6133 + "id": 272, 6134 + "from_node_id": 283, 6135 + "to_node_id": 284, 6136 + "from_change_id": "e3adddaf-9126-4bfa-8d75-aa8b94323077", 6137 + "to_change_id": "3d9caa98-6f9c-4613-9c05-92566f9ee0d5", 6138 + "edge_type": "leads_to", 6139 + "weight": 1.0, 6140 + "rationale": "Key observation about flow", 6141 + "created_at": "2025-12-26T00:18:57.901372200-05:00" 6142 + }, 6143 + { 6144 + "id": 273, 6145 + "from_node_id": 284, 6146 + "to_node_id": 285, 6147 + "from_change_id": "3d9caa98-6f9c-4613-9c05-92566f9ee0d5", 6148 + "to_change_id": "f0da412f-562b-4e45-b83d-eba28fc22eea", 6149 + "edge_type": "leads_to", 6150 + "weight": 1.0, 6151 + "rationale": "Build completed after refactoring extension flow", 6152 + "created_at": "2025-12-26T00:24:07.711141900-05:00" 6153 + }, 6154 + { 6155 + "id": 274, 6156 + "from_node_id": 285, 6157 + "to_node_id": 286, 6158 + "from_change_id": "f0da412f-562b-4e45-b83d-eba28fc22eea", 6159 + "to_change_id": "60c9ec75-7e3f-4aa4-b8cf-0691ef92d260", 6160 + "edge_type": "leads_to", 6161 + "weight": 1.0, 6162 + "rationale": "Committed after successful build", 6163 + "created_at": "2025-12-26T00:26:22.985765600-05:00" 5977 6164 } 5978 6165 ] 5979 6166 }
+33
packages/extension/src/lib/api-client.ts
··· 40 40 try { 41 41 const response = await fetch(url, { 42 42 method: 'POST', 43 + credentials: 'include', // Include cookies for auth 43 44 headers: { 44 45 'Content-Type': 'application/json' 45 46 }, ··· 99 100 export function getApiUrl(): string { 100 101 return ATLAST_API_URL; 101 102 } 103 + 104 + /** 105 + * Check if user is logged in to ATlast 106 + * Returns user profile if logged in, null otherwise 107 + */ 108 + export async function checkSession(): Promise<{ 109 + did: string; 110 + handle: string; 111 + displayName?: string; 112 + avatar?: string; 113 + } | null> { 114 + try { 115 + const response = await fetch(`${ATLAST_API_URL}/.netlify/functions/session`, { 116 + method: 'GET', 117 + credentials: 'include', // Include cookies 118 + headers: { 119 + 'Accept': 'application/json' 120 + } 121 + }); 122 + 123 + if (!response.ok) { 124 + console.log('[API Client] Not logged in'); 125 + return null; 126 + } 127 + 128 + const data = await response.json(); 129 + return data; 130 + } catch (error) { 131 + console.error('[API Client] Session check failed:', error); 132 + return null; 133 + } 134 + }
+11
packages/extension/src/popup/popup.html
··· 76 76 <p class="hint" id="server-url"></p> 77 77 <button id="btn-check-server" class="btn-primary">Check Again</button> 78 78 </div> 79 + 80 + <!-- Not logged in state --> 81 + <div id="state-not-logged-in" class="state hidden"> 82 + <div class="icon">🔐</div> 83 + <p class="message">Not logged in to ATlast</p> 84 + <p class="error-message"> 85 + Please log in to ATlast first, then return here to scan. 86 + </p> 87 + <button id="btn-open-atlast" class="btn-primary">Open ATlast</button> 88 + <button id="btn-retry-login" class="btn-secondary">Check Again</button> 89 + </div> 79 90 </main> 80 91 81 92 <footer>
+41 -4
packages/extension/src/popup/popup.ts
··· 15 15 complete: document.getElementById('state-complete')!, 16 16 uploading: document.getElementById('state-uploading')!, 17 17 error: document.getElementById('state-error')!, 18 - offline: document.getElementById('state-offline')! 18 + offline: document.getElementById('state-offline')!, 19 + notLoggedIn: document.getElementById('state-not-logged-in')! 19 20 }; 20 21 21 22 const elements = { ··· 29 30 btnStart: document.getElementById('btn-start')! as HTMLButtonElement, 30 31 btnUpload: document.getElementById('btn-upload')! as HTMLButtonElement, 31 32 btnRetry: document.getElementById('btn-retry')! as HTMLButtonElement, 32 - btnCheckServer: document.getElementById('btn-check-server')! as HTMLButtonElement 33 + btnCheckServer: document.getElementById('btn-check-server')! as HTMLButtonElement, 34 + btnOpenAtlast: document.getElementById('btn-open-atlast')! as HTMLButtonElement, 35 + btnRetryLogin: document.getElementById('btn-retry-login')! as HTMLButtonElement 33 36 }; 34 37 35 38 /** ··· 157 160 158 161 console.log('[Popup] Upload successful:', response.importId); 159 162 160 - // Open ATlast with import ID 161 - chrome.tabs.create({ url: response.redirectUrl }); 163 + // Open ATlast at results page with upload data 164 + const { getApiUrl } = await import('../lib/api-client.js'); 165 + const resultsUrl = `${getApiUrl()}${response.redirectUrl}`; 166 + chrome.tabs.create({ url: resultsUrl }); 162 167 163 168 } catch (error) { 164 169 console.error('[Popup] Error uploading:', error); ··· 247 252 return; 248 253 } 249 254 } 255 + 256 + // Check if user is logged in to ATlast 257 + console.log('[Popup] 🔐 Checking login status...'); 258 + const { checkSession } = await import('../lib/api-client.js'); 259 + const session = await checkSession(); 260 + 261 + if (!session) { 262 + console.log('[Popup] ❌ Not logged in'); 263 + showState('notLoggedIn'); 264 + 265 + // Set up login buttons 266 + elements.btnOpenAtlast.addEventListener('click', () => { 267 + chrome.tabs.create({ url: getApiUrl() }); 268 + }); 269 + 270 + elements.btnRetryLogin.addEventListener('click', async () => { 271 + elements.btnRetryLogin.disabled = true; 272 + elements.btnRetryLogin.textContent = 'Checking...'; 273 + 274 + const newSession = await checkSession(); 275 + if (newSession) { 276 + // User is now logged in, re-initialize 277 + init(); 278 + } else { 279 + elements.btnRetryLogin.disabled = false; 280 + elements.btnRetryLogin.textContent = 'Check Again'; 281 + } 282 + }); 283 + return; 284 + } 285 + 286 + console.log('[Popup] ✅ Logged in as', session.handle); 250 287 251 288 // Get current state 252 289 console.log('[Popup] 📡 Requesting state from background...');
+58 -77
packages/functions/src/extension-import.ts
··· 1 - import type { Handler, HandlerEvent } from '@netlify/functions'; 1 + import { AuthenticatedHandler } from './core/types'; 2 2 import type { ExtensionImportRequest, ExtensionImportResponse } from '@atlast/shared'; 3 3 import { z } from 'zod'; 4 - import { storeImport } from './utils/import-store.js'; 4 + import crypto from 'crypto'; 5 + import { withAuthErrorHandling } from './core/middleware'; 6 + import { ValidationError } from './core/errors'; 7 + import { UploadRepository, SourceAccountRepository } from './repositories'; 8 + import { normalize } from './utils/string.utils'; 9 + import { successResponse } from './utils'; 5 10 6 11 /** 7 12 * Validation schema for extension import request ··· 20 25 /** 21 26 * Extension import endpoint 22 27 * POST /extension-import 28 + * 29 + * Requires authentication. Creates upload and saves usernames immediately. 23 30 */ 24 - export const handler: Handler = async (event: HandlerEvent) => { 25 - // CORS headers 26 - const headers = { 27 - 'Access-Control-Allow-Origin': '*', // TODO: Restrict in production 28 - 'Access-Control-Allow-Headers': 'Content-Type', 29 - 'Access-Control-Allow-Methods': 'POST, OPTIONS', 30 - 'Content-Type': 'application/json', 31 - }; 31 + const extensionImportHandler: AuthenticatedHandler = async (context) => { 32 + const body: ExtensionImportRequest = JSON.parse(context.event.body || '{}'); 32 33 33 - // Handle OPTIONS preflight 34 - if (event.httpMethod === 'OPTIONS') { 35 - return { 36 - statusCode: 204, 37 - headers, 38 - body: '', 39 - }; 40 - } 34 + // Validate request 35 + const validatedData = ExtensionImportSchema.parse(body); 41 36 42 - // Only allow POST 43 - if (event.httpMethod !== 'POST') { 44 - return { 45 - statusCode: 405, 46 - headers, 47 - body: JSON.stringify({ error: 'Method not allowed' }), 48 - }; 49 - } 37 + console.log('[extension-import] Received import:', { 38 + did: context.did, 39 + platform: validatedData.platform, 40 + usernameCount: validatedData.usernames.length, 41 + pageType: validatedData.metadata.pageType, 42 + extensionVersion: validatedData.metadata.extensionVersion 43 + }); 50 44 51 - try { 52 - // Parse and validate request body 53 - const body = JSON.parse(event.body || '{}'); 54 - const validatedData = ExtensionImportSchema.parse(body); 45 + // Generate upload ID 46 + const uploadId = crypto.randomBytes(16).toString('hex'); 55 47 56 - console.log('[extension-import] Received import:', { 57 - platform: validatedData.platform, 58 - usernameCount: validatedData.usernames.length, 59 - pageType: validatedData.metadata.pageType, 60 - extensionVersion: validatedData.metadata.extensionVersion 61 - }); 48 + // Create upload and save source accounts 49 + const uploadRepo = new UploadRepository(); 50 + const sourceAccountRepo = new SourceAccountRepository(); 62 51 63 - // Store the import data 64 - const importId = storeImport(validatedData); 52 + // Create upload record 53 + await uploadRepo.createUpload( 54 + uploadId, 55 + context.did, 56 + validatedData.platform, 57 + validatedData.usernames.length 58 + ); 65 59 66 - // Get base URL from event (handles local and production) 67 - const baseUrl = event.headers.host?.includes('localhost') || event.headers.host?.includes('127.0.0.1') 68 - ? `http://${event.headers.host}` 69 - : `https://${event.headers.host}`; 60 + console.log(`[extension-import] Created upload ${uploadId} for user ${context.did}`); 70 61 71 - const redirectUrl = `${baseUrl}/import/${importId}`; 62 + // Save source accounts 63 + let savedCount = 0; 64 + for (const username of validatedData.usernames) { 65 + const normalizedUsername = normalize(username); 72 66 73 - // Return response 74 - const response: ExtensionImportResponse = { 75 - importId, 76 - usernameCount: validatedData.usernames.length, 77 - redirectUrl 78 - }; 67 + try { 68 + await sourceAccountRepo.upsertSourceAccount( 69 + validatedData.platform, 70 + username, 71 + normalizedUsername 72 + ); 73 + savedCount++; 74 + } catch (error) { 75 + console.error(`[extension-import] Error saving username ${username}:`, error); 76 + // Continue with other usernames 77 + } 78 + } 79 79 80 - return { 81 - statusCode: 200, 82 - headers, 83 - body: JSON.stringify(response), 84 - }; 85 - } catch (error) { 86 - console.error('[extension-import] Error:', error); 80 + console.log(`[extension-import] Saved ${savedCount}/${validatedData.usernames.length} source accounts`); 87 81 88 - // Handle validation errors 89 - if (error instanceof z.ZodError) { 90 - return { 91 - statusCode: 400, 92 - headers, 93 - body: JSON.stringify({ 94 - error: 'Validation error', 95 - details: error.errors 96 - }), 97 - }; 98 - } 82 + // Return upload data for frontend to search 83 + const response: ExtensionImportResponse = { 84 + importId: uploadId, 85 + usernameCount: validatedData.usernames.length, 86 + redirectUrl: `/results?uploadId=${uploadId}` // Frontend will handle this 87 + }; 99 88 100 - // Handle other errors 101 - return { 102 - statusCode: 500, 103 - headers, 104 - body: JSON.stringify({ 105 - error: 'Internal server error', 106 - message: error instanceof Error ? error.message : 'Unknown error' 107 - }), 108 - }; 109 - } 89 + return successResponse(response); 110 90 }; 111 91 92 + export const handler = withAuthErrorHandling(extensionImportHandler);
-88
packages/functions/src/get-extension-import.ts
··· 1 - import type { Handler, HandlerEvent } from '@netlify/functions'; 2 - import type { ExtensionImportRequest } from '@atlast/shared'; 3 - import { getImport } from './utils/import-store.js'; 4 - 5 - /** 6 - * Get extension import by ID 7 - * GET /get-extension-import?importId=xxx 8 - */ 9 - export const handler: Handler = async (event: HandlerEvent) => { 10 - const headers = { 11 - 'Access-Control-Allow-Origin': '*', 12 - 'Access-Control-Allow-Headers': 'Content-Type', 13 - 'Access-Control-Allow-Methods': 'GET, OPTIONS', 14 - 'Content-Type': 'application/json', 15 - }; 16 - 17 - // Handle OPTIONS preflight 18 - if (event.httpMethod === 'OPTIONS') { 19 - return { 20 - statusCode: 204, 21 - headers, 22 - body: '', 23 - }; 24 - } 25 - 26 - // Only allow GET 27 - if (event.httpMethod !== 'GET') { 28 - return { 29 - statusCode: 405, 30 - headers, 31 - body: JSON.stringify({ error: 'Method not allowed' }), 32 - }; 33 - } 34 - 35 - try { 36 - // Get import ID from query params 37 - const importId = event.queryStringParameters?.importId; 38 - 39 - if (!importId) { 40 - return { 41 - statusCode: 400, 42 - headers, 43 - body: JSON.stringify({ error: 'Missing importId parameter' }), 44 - }; 45 - } 46 - 47 - // Get import data from shared store 48 - const importData = getImport(importId); 49 - 50 - if (!importData) { 51 - return { 52 - statusCode: 404, 53 - headers, 54 - body: JSON.stringify({ error: 'Import not found or expired' }), 55 - }; 56 - } 57 - 58 - // Return import data 59 - return { 60 - statusCode: 200, 61 - headers, 62 - body: JSON.stringify(importData), 63 - }; 64 - } catch (error) { 65 - console.error('[get-extension-import] Error:', error); 66 - 67 - return { 68 - statusCode: 500, 69 - headers, 70 - body: JSON.stringify({ 71 - error: 'Internal server error', 72 - message: error instanceof Error ? error.message : 'Unknown error' 73 - }), 74 - }; 75 - } 76 - }; 77 - 78 - /** 79 - * NOTE: This is a temporary implementation using in-memory storage. 80 - * In production, both extension-import.ts and this function would share 81 - * the same database for storing and retrieving imports. 82 - * 83 - * Suggested production implementation: 84 - * - Add extension_imports table to database 85 - * - Store: platform, usernames (JSON), metadata (JSON), created_at, expires_at 86 - * - Index on import_id for fast lookups 87 - * - Auto-expire using database TTL or cron job 88 - */
-70
packages/functions/src/utils/import-store.ts
··· 1 - import type { ExtensionImportRequest } from '@atlast/shared'; 2 - import crypto from 'crypto'; 3 - 4 - /** 5 - * Shared in-memory store for extension imports 6 - * This is shared between extension-import.ts and get-extension-import.ts 7 - * 8 - * NOTE: In-memory storage works for development but will NOT work reliably 9 - * in production serverless environments where functions are stateless and 10 - * can run on different instances. 11 - * 12 - * For production, replace this with: 13 - * - Database (PostgreSQL/Neon) 14 - * - Redis/Upstash 15 - * - Netlify Blobs 16 - */ 17 - const importStore = new Map<string, ExtensionImportRequest>(); 18 - 19 - /** 20 - * Generate a random import ID 21 - */ 22 - export function generateImportId(): string { 23 - return crypto.randomBytes(16).toString('hex'); 24 - } 25 - 26 - /** 27 - * Store import data and return import ID 28 - */ 29 - export function storeImport(data: ExtensionImportRequest): string { 30 - const importId = generateImportId(); 31 - importStore.set(importId, data); 32 - 33 - console.log(`[ImportStore] Stored import ${importId} with ${data.usernames.length} usernames`); 34 - 35 - // Auto-expire after 1 hour 36 - setTimeout(() => { 37 - importStore.delete(importId); 38 - console.log(`[ImportStore] Expired import ${importId}`); 39 - }, 60 * 60 * 1000); 40 - 41 - return importId; 42 - } 43 - 44 - /** 45 - * Get import data by ID 46 - */ 47 - export function getImport(importId: string): ExtensionImportRequest | null { 48 - const data = importStore.get(importId) || null; 49 - console.log(`[ImportStore] Get import ${importId}: ${data ? 'found' : 'not found'}`); 50 - return data; 51 - } 52 - 53 - /** 54 - * Delete import data by ID 55 - */ 56 - export function deleteImport(importId: string): boolean { 57 - const result = importStore.delete(importId); 58 - console.log(`[ImportStore] Delete import ${importId}: ${result ? 'success' : 'not found'}`); 59 - return result; 60 - } 61 - 62 - /** 63 - * Get store stats (for debugging) 64 - */ 65 - export function getStoreStats(): { count: number; ids: string[] } { 66 - return { 67 - count: importStore.size, 68 - ids: Array.from(importStore.keys()) 69 - }; 70 - }
-4
packages/web/src/Router.tsx
··· 1 1 import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 2 import App from './App'; 3 - import ExtensionImport from './pages/ExtensionImport'; 4 3 5 4 /** 6 5 * Application Router ··· 12 11 <Routes> 13 12 {/* Main app route */} 14 13 <Route path="/" element={<App />} /> 15 - 16 - {/* Extension import route */} 17 - <Route path="/import/:importId" element={<ExtensionImport />} /> 18 14 </Routes> 19 15 </BrowserRouter> 20 16 );
-138
packages/web/src/pages/ExtensionImport.tsx
··· 1 - import { useEffect, useState } from 'react'; 2 - import { useParams, useNavigate } from 'react-router-dom'; 3 - import type { ExtensionImportRequest } from '@atlast/shared'; 4 - import { apiClient } from '../lib/api/client'; 5 - 6 - /** 7 - * Extension Import page 8 - * Receives data from browser extension and processes it 9 - */ 10 - export default function ExtensionImport() { 11 - const { importId } = useParams<{ importId: string }>(); 12 - const navigate = useNavigate(); 13 - 14 - const [loading, setLoading] = useState(true); 15 - const [error, setError] = useState<string | null>(null); 16 - const [importData, setImportData] = useState<ExtensionImportRequest | null>(null); 17 - 18 - useEffect(() => { 19 - if (!importId) { 20 - setError('No import ID provided'); 21 - setLoading(false); 22 - return; 23 - } 24 - 25 - fetchImportData(importId); 26 - }, [importId]); 27 - 28 - async function fetchImportData(id: string) { 29 - try { 30 - setLoading(true); 31 - setError(null); 32 - 33 - const response = await fetch( 34 - `/.netlify/functions/get-extension-import?importId=${id}` 35 - ); 36 - 37 - if (!response.ok) { 38 - if (response.status === 404) { 39 - throw new Error('Import not found or expired. Please try scanning again.'); 40 - } 41 - throw new Error('Failed to load import data'); 42 - } 43 - 44 - const data: ExtensionImportRequest = await response.json(); 45 - setImportData(data); 46 - 47 - // Automatically start the search process 48 - startSearch(data); 49 - } catch (err) { 50 - console.error('[ExtensionImport] Error:', err); 51 - setError(err instanceof Error ? err.message : 'Unknown error'); 52 - setLoading(false); 53 - } 54 - } 55 - 56 - async function startSearch(data: ExtensionImportRequest) { 57 - try { 58 - // Navigate to results page with the extension data 59 - // The results page will handle the search 60 - navigate('/results', { 61 - state: { 62 - usernames: data.usernames, 63 - platform: data.platform, 64 - source: 'extension' 65 - } 66 - }); 67 - } catch (err) { 68 - console.error('[ExtensionImport] Search error:', err); 69 - setError('Failed to start search. Please try again.'); 70 - setLoading(false); 71 - } 72 - } 73 - 74 - if (loading && !error) { 75 - return ( 76 - <div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-cyan-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-900"> 77 - <div className="container mx-auto px-4 py-16"> 78 - <div className="max-w-md mx-auto text-center"> 79 - <div className="mb-8"> 80 - <div className="inline-block animate-spin rounded-full h-16 w-16 border-b-2 border-purple-600 dark:border-cyan-400"></div> 81 - </div> 82 - <h1 className="text-2xl font-bold text-purple-900 dark:text-cyan-50 mb-4"> 83 - Loading your import... 84 - </h1> 85 - <p className="text-purple-700 dark:text-cyan-200"> 86 - Processing data from the extension 87 - </p> 88 - </div> 89 - </div> 90 - </div> 91 - ); 92 - } 93 - 94 - if (error) { 95 - return ( 96 - <div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-cyan-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-900"> 97 - <div className="container mx-auto px-4 py-16"> 98 - <div className="max-w-md mx-auto text-center"> 99 - <div className="mb-8 text-6xl">⚠️</div> 100 - <h1 className="text-2xl font-bold text-purple-900 dark:text-cyan-50 mb-4"> 101 - Import Error 102 - </h1> 103 - <p className="text-purple-700 dark:text-cyan-200 mb-8"> 104 - {error} 105 - </p> 106 - <button 107 - onClick={() => navigate('/')} 108 - className="px-6 py-3 bg-purple-600 hover:bg-purple-700 dark:bg-cyan-600 dark:hover:bg-cyan-700 text-white rounded-lg font-medium transition-colors" 109 - > 110 - Go Home 111 - </button> 112 - </div> 113 - </div> 114 - </div> 115 - ); 116 - } 117 - 118 - // This shouldn't be reached since we navigate away on success 119 - return ( 120 - <div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-cyan-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-900"> 121 - <div className="container mx-auto px-4 py-16"> 122 - <div className="max-w-md mx-auto text-center"> 123 - <div className="mb-8"> 124 - <div className="inline-block animate-spin rounded-full h-16 w-16 border-b-2 border-purple-600 dark:border-cyan-400"></div> 125 - </div> 126 - <h1 className="text-2xl font-bold text-purple-900 dark:text-cyan-50 mb-4"> 127 - Starting search... 128 - </h1> 129 - {importData && ( 130 - <p className="text-purple-700 dark:text-cyan-200"> 131 - Searching for {importData.usernames.length} users from {importData.platform} 132 - </p> 133 - )} 134 - </div> 135 - </div> 136 - </div> 137 - ); 138 - }
+39
pnpm-lock.yaml
··· 202 202 react-dom: 203 203 specifier: ^18.3.1 204 204 version: 18.3.1(react@18.3.1) 205 + react-router-dom: 206 + specifier: ^7.11.0 207 + version: 7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 205 208 zustand: 206 209 specifier: ^5.0.9 207 210 version: 5.0.9(@types/react@19.2.7)(react@18.3.1) ··· 2489 2492 react-refresh@0.17.0: 2490 2493 resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} 2491 2494 engines: {node: '>=0.10.0'} 2495 + 2496 + react-router-dom@7.11.0: 2497 + resolution: {integrity: sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==} 2498 + engines: {node: '>=20.0.0'} 2499 + peerDependencies: 2500 + react: '>=18' 2501 + react-dom: '>=18' 2502 + 2503 + react-router@7.11.0: 2504 + resolution: {integrity: sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==} 2505 + engines: {node: '>=20.0.0'} 2506 + peerDependencies: 2507 + react: '>=18' 2508 + react-dom: '>=18' 2509 + peerDependenciesMeta: 2510 + react-dom: 2511 + optional: true 2492 2512 2493 2513 react@18.3.1: 2494 2514 resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} ··· 2588 2608 engines: {node: '>=10'} 2589 2609 hasBin: true 2590 2610 2611 + set-cookie-parser@2.7.2: 2612 + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} 2613 + 2591 2614 setimmediate@1.0.5: 2592 2615 resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} 2593 2616 ··· 5123 5146 5124 5147 react-refresh@0.17.0: {} 5125 5148 5149 + react-router-dom@7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 5150 + dependencies: 5151 + react: 18.3.1 5152 + react-dom: 18.3.1(react@18.3.1) 5153 + react-router: 7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 5154 + 5155 + react-router@7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 5156 + dependencies: 5157 + cookie: 1.1.1 5158 + react: 18.3.1 5159 + set-cookie-parser: 2.7.2 5160 + optionalDependencies: 5161 + react-dom: 18.3.1(react@18.3.1) 5162 + 5126 5163 react@18.3.1: 5127 5164 dependencies: 5128 5165 loose-envify: 1.4.0 ··· 5248 5285 semver@6.3.1: {} 5249 5286 5250 5287 semver@7.7.3: {} 5288 + 5289 + set-cookie-parser@2.7.2: {} 5251 5290 5252 5291 setimmediate@1.0.5: {} 5253 5292