diff --git a/v2/.gitignore b/v2/.gitignore new file mode 100644 index 0000000..dafa699 --- /dev/null +++ b/v2/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +.env.local diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..715047c --- /dev/null +++ b/v2/README.md @@ -0,0 +1,47 @@ +# SecureBin v2 + +SecureBin is a Google Chrome extension for interfacing securely with PasteBin. +Users can encrypt plaintext and have it stored onto PasteBin, where they can copy the link and key to send it to another user for decryption. + +## What's New in v2 + +Version 2 is a complete rewrite of the extension using modern web technologies: +- **Framework:** React 19 with TypeScript +- **Styling:** Tailwind CSS v4 and Radix UI primitives for a modern, beautiful, and accessible UI +- **State Management:** Zustand +- **Routing:** React Router v7 +- **Bundler:** Vite with `@crxjs/vite-plugin` for optimized extension builds and HMR (Hot Module Replacement) + +## Prerequisites + +To use this extension, you will need a PasteBin Developer API Key to post your payloads securely. +1. Sign up or Log in to [PasteBin](https://pastebin.com/). +2. Navigate to the [API Documentation](https://pastebin.com/doc_api#1) to copy your developer API key. +3. Open the extension's Settings page to input and save your API key. + +## Development + +### Setup + +Ensure you have Node.js (and `npm`) installed, then install the dependencies: + +```bash +cd v2 +npm install +``` + +### Scripts + +- `npm run dev`: Start the Vite development server with Hot Module Replacement (HMR). You should load the generated `v2/dist` folder in Chrome. +- `npm run build`: Build the extension for production. +- `npm run lint`: Run ESLint to analyze the code and automatically fix format issues. +- `npm run test`: Run unit tests using Vitest. +- `npm run test:watch`: Run tests in watch mode. +- `npm run preview`: Preview the production build. + +## Installation + +1. First, build the extension using `npm run build` or start the dev server via `npm run dev`. +2. Open Chrome and navigate to your extensions page: `chrome://extensions/`. +3. Enable **Developer mode** in the top-right corner. +4. Click **Load unpacked** and select the `v2/dist` directory. diff --git a/v2/icons/logo128.png b/v2/icons/logo128.png new file mode 100755 index 0000000..94a51ab Binary files /dev/null and b/v2/icons/logo128.png differ diff --git a/v2/icons/logo192.png b/v2/icons/logo192.png new file mode 100644 index 0000000..0663a23 Binary files /dev/null and b/v2/icons/logo192.png differ diff --git a/v2/icons/logo48.png b/v2/icons/logo48.png new file mode 100755 index 0000000..6608db1 Binary files /dev/null and b/v2/icons/logo48.png differ diff --git a/v2/index.html b/v2/index.html new file mode 100644 index 0000000..fa2912c --- /dev/null +++ b/v2/index.html @@ -0,0 +1,12 @@ + + + + + + SecureBin + + +
+ + + diff --git a/v2/manifest.json b/v2/manifest.json new file mode 100644 index 0000000..3d0656c --- /dev/null +++ b/v2/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "SecureBin: Powerful Pastebin Tool", + "description": "Securely encrypt your text on Pastebin.com", + "version": "2.0.0", + "manifest_version": 3, + "action": { + "default_popup": "index.html", + "default_title": "Open SecureBin" + }, + "icons": { + "16": "icons/logo48.png", + "48": "icons/logo48.png", + "128": "icons/logo128.png" + }, + "background": { + "service_worker": "src/background.ts", + "type": "module" + }, + "permissions": [ + "storage", + "contextMenus", + "clipboardWrite", + "clipboardRead", + "activeTab" + ], + "optional_permissions": [ + "" + ], + "host_permissions": [ + "https://pastebin.com/*", + "https://cors.securebin.workers.dev/*" + ] +} diff --git a/v2/package.json b/v2/package.json new file mode 100644 index 0000000..1c641de --- /dev/null +++ b/v2/package.json @@ -0,0 +1,42 @@ +{ + "name": "securebin", + "version": "2.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --fix", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.0", + "tailwind-merge": "^3.0.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.30", + "@tailwindcss/vite": "^4.0.0", + "@types/chrome": "^0.0.287", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.20", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0", + "vitest": "^4.1.2" + } +} diff --git a/v2/public/securebinlogo.svg b/v2/public/securebinlogo.svg new file mode 100644 index 0000000..2eb8a46 --- /dev/null +++ b/v2/public/securebinlogo.svg @@ -0,0 +1,20 @@ + + + Group + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v2/public/securebinlogo_dark.svg b/v2/public/securebinlogo_dark.svg new file mode 100644 index 0000000..1ac491b --- /dev/null +++ b/v2/public/securebinlogo_dark.svg @@ -0,0 +1,20 @@ + + + Group + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v2/src/App.tsx b/v2/src/App.tsx new file mode 100644 index 0000000..45191fd --- /dev/null +++ b/v2/src/App.tsx @@ -0,0 +1,81 @@ +import { useEffect } from 'react' +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom' +import { useStore } from './lib/store' +import { cn } from './lib/cn' +import NavBar from './components/NavBar' +import Editor from './routes/Editor' +import Settings from './routes/Settings' +import History from './routes/History' +import Result from './routes/Result' +import ApiKeyConfig from './routes/ApiKeyConfig' +import EncConfig from './routes/EncConfig' +import Support from './routes/Support' + +// Routes we won't try to restore (transient pages) +const NON_RESTORABLE_ROUTES = new Set(['/', '/home', '/result']) + +export default function App() { + const { settings, initialized, initialize } = useStore() + const isDark = settings.theme === 'dark' + const navigate = useNavigate() + const location = useLocation() + + useEffect(() => { + initialize() + }, [initialize]) + + useEffect(() => { + document.documentElement.classList.toggle('dark', isDark) + }, [isDark]) + + // Restore last page on popup open (after settings are loaded) + useEffect(() => { + if (!initialized) return + const { page_timeout } = settings + if (page_timeout === 0) return + + chrome.storage.session.get(['lastRoute', 'lastRouteTime'], (data) => { + const lastRoute: string = data.lastRoute ?? '' + const lastRouteTime: number = data.lastRouteTime ?? 0 + if (!lastRoute || NON_RESTORABLE_ROUTES.has(lastRoute)) return + + const ageMs = Date.now() - lastRouteTime + if (page_timeout === -1 || ageMs < page_timeout * 1000) { + navigate(lastRoute, { replace: true }) + } + }) + }, [initialized]) + + // Persist current route so we can restore it next time + useEffect(() => { + if (!initialized) return + chrome.storage.session.set({ lastRoute: location.pathname, lastRouteTime: Date.now() }) + }, [location.pathname, initialized]) + + if (!initialized) { + return ( +
+
+
+ ) + } + + return ( +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ) +} diff --git a/v2/src/background.ts b/v2/src/background.ts new file mode 100644 index 0000000..4d738d4 --- /dev/null +++ b/v2/src/background.ts @@ -0,0 +1,54 @@ +import { StorageKey, EncryptionMode } from './lib/constants' + +// Initialize defaults on install +chrome.runtime.onInstalled.addListener(async () => { + const data = await chrome.storage.sync.get(StorageKey.SETTINGS) + if (!data[StorageKey.SETTINGS]) { + await chrome.storage.sync.set({ + [StorageKey.SETTINGS]: JSON.stringify({ + apiKey: 'MmU1OGNlMjcyMzllMzRhNzdjNWVmNjVkYmVhOGIyNGQ=', + encMode: EncryptionMode.AES_GCM, + keyLength: 16, + theme: 'light', + encryption: false, + default_action: 'Post to Pastebin', + page_timeout: 30, + }), + }) + } + + // Rebuild context menus on install/update to avoid duplicate registration errors + chrome.contextMenus.removeAll(() => { + chrome.contextMenus.create({ + id: 'securebinOpen', + title: 'Open in SecureBin', + contexts: ['selection'], + }) + }) +}) + +// Also register context menu on startup (service worker restart) +chrome.runtime.onStartup.addListener(() => { + chrome.contextMenus.removeAll(() => { + chrome.contextMenus.create({ + id: 'securebinOpen', + title: 'Open in SecureBin', + contexts: ['selection'], + }) + }) +}) + +chrome.contextMenus.onClicked.addListener(async (info) => { + const text = info.selectionText + if (!text || info.menuItemId !== 'securebinOpen') return + + // Store the selected text so the popup can load it into the editor + await chrome.storage.session.set({ pendingText: { text } }) + + // Open the extension popup + try { + await chrome.action.openPopup() + } catch { + // openPopup() may fail if popup is already open — safe to ignore + } +}) diff --git a/v2/src/components/NavBar.tsx b/v2/src/components/NavBar.tsx new file mode 100644 index 0000000..09cd054 --- /dev/null +++ b/v2/src/components/NavBar.tsx @@ -0,0 +1,49 @@ +import { useNavigate, useLocation } from 'react-router-dom' +import { PenLine, Clock, Settings } from 'lucide-react' +import { cn } from '@/lib/cn' +import { useStore } from '@/lib/store' + +export default function NavBar() { + const navigate = useNavigate() + const location = useLocation() + const isDark = useStore((s) => s.settings.theme === 'dark') + + const navItems = [ + { icon: PenLine, path: '/home', label: 'Editor' }, + { icon: Clock, path: '/history', label: 'History' }, + { icon: Settings, path: '/settings', label: 'Settings' }, + ] + + return ( +
+
+ SecureBin +
+ +
+ ) +} diff --git a/v2/src/components/common/CopyBox.tsx b/v2/src/components/common/CopyBox.tsx new file mode 100644 index 0000000..329b56f --- /dev/null +++ b/v2/src/components/common/CopyBox.tsx @@ -0,0 +1,102 @@ +import { useState, useCallback } from 'react' +import { Copy, Check, ExternalLink, Eye, EyeOff } from 'lucide-react' +import { cn } from '@/lib/cn' + +interface CopyBoxProps { + label?: string + value: string + multiline?: boolean + rows?: number + masked?: boolean + allowCopy?: boolean + openInNew?: boolean + className?: string +} + +export default function CopyBox({ + label, + value, + multiline = false, + rows = 4, + masked = false, + allowCopy = true, + openInNew = false, + className, +}: CopyBoxProps) { + const [copied, setCopied] = useState(false) + const [visible, setVisible] = useState(!masked) + + const handleCopy = useCallback(async () => { + await navigator.clipboard.writeText(value) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [value]) + + const handleOpenNew = useCallback(() => { + window.open(value, '_blank', 'noopener,noreferrer') + }, [value]) + + return ( +
+ {label && ( + + )} +
+ {multiline ? ( +