Skip to content
Open
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
4 changes: 4 additions & 0 deletions v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
.env
.env.local
47 changes: 47 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added v2/icons/logo128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added v2/icons/logo192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added v2/icons/logo48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions v2/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureBin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions v2/manifest.json
Original file line number Diff line number Diff line change
@@ -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": [
"<all_urls>"
],
"host_permissions": [
"https://pastebin.com/*",
"https://cors.securebin.workers.dev/*"
]
}
42 changes: 42 additions & 0 deletions v2/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
20 changes: 20 additions & 0 deletions v2/public/securebinlogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions v2/public/securebinlogo_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions v2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cn('h-full flex items-center justify-center', isDark && 'dark')}>
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
)
}

return (
<div className={cn('flex flex-col h-full bg-surface text-text-primary')}>
<NavBar />
<main className="flex-1 overflow-y-auto">
<Routes>
<Route path="/" element={<Editor />} />
<Route path="/home" element={<Editor />} />
<Route path="/settings" element={<Settings />} />
<Route path="/apikey" element={<ApiKeyConfig />} />
<Route path="/encconfig" element={<EncConfig />} />
<Route path="/support" element={<Support />} />
<Route path="/history" element={<History />} />
<Route path="/result" element={<Result />} />
<Route path="/result/:index" element={<Result />} />
</Routes>
</main>
</div>
)
}
54 changes: 54 additions & 0 deletions v2/src/background.ts
Original file line number Diff line number Diff line change
@@ -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
}
})
49 changes: 49 additions & 0 deletions v2/src/components/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="flex items-center justify-between px-4 py-3 border-b border-border bg-surface/80 backdrop-blur-xl">
<div className="flex items-center">
<img
src={isDark ? '/securebinlogo_dark.svg' : '/securebinlogo.svg'}
alt="SecureBin"
className="h-5"
/>
</div>
<nav className="flex items-center gap-1">
{navItems.map(({ icon: Icon, path, label }) => {
const isActive = location.pathname === path || (path === '/home' && location.pathname === '/')
return (
<button
key={path}
onClick={() => navigate(path)}
className={cn(
'p-2 rounded-lg transition-all duration-150',
'hover:bg-surface-hover active:scale-95',
isActive
? 'text-primary bg-primary-light/50'
: 'text-text-muted hover:text-text-secondary',
)}
title={label}
>
<Icon size={18} strokeWidth={isActive ? 2.5 : 2} />
</button>
)
})}
</nav>
</header>
)
}
Loading