This commit is contained in:
Asad 2025-01-17 15:05:21 -05:00
commit 9fb7bd46e0
33 changed files with 3931 additions and 0 deletions

5
.gitignore vendored Normal file

@ -0,0 +1,5 @@
node_modules
playground.db
.env

70
README.md Normal file

@ -0,0 +1,70 @@
# BinCode - A Complete Self-Hostable JSFiddle Alternative
## Overview
BinCode is a minimalist platform for managing and sharing code snippets, built with Vue.js and SQLite. Unlike similar tools that depend on external services, BinCode runs entirely on your own infrastructure, giving you complete control over your data and workflow.
## Key Features
- **Fully Self-Contained:** No external dependencies or services required
- **Modern Stack:** Built with Vue.js frontend and SQLite database
- **Secure Authentication:** Simple JWT-based user management
- **Instant Sharing:** Generate unique URLs for any code snippet
- **Minimal Setup:** Just create users manually and start sharing code
- **Performance Focused:** Lightweight design ensures fast operation
## Architecture
- Frontend: Vue.js
- Database: SQLite
- Authentication: JWT
- Deployment: Single server, self-contained
There is no signups its by design just create users manullay and they can start commits snippets to share
## Why BinCode?
During my search for open-source code sharing solutions, I found that most platforms required external services or had complex deployment requirements. I needed a straightforward, self-hostable tool that focused solely on creating and sharing code snippets.
BinCode was built with simplicity in mind. While it may not have all the features of larger platforms, its lightweight nature makes it perfect for teams and individuals who need a reliable, easy-to-maintain solution for code sharing.
## Design Choices
- **No Signups:** User accounts are created manually by administrators to keep the system simple and secure
- **Self-Contained:** Everything runs on your infrastructure, ensuring data privacy and control
## Contributing
While BinCode is intentionally minimal, contributions are always welcome! Feel free to add features that enhance its functionality while maintaining its lightweight nature.
## Getting Started
### 1. Clone and Install Dependencies
```bash
git clone https://git.sheetjs.com/asadbek064/BinCode.git
cd BinCode
pnpm install
```
### 2. Set Up Environment Variables
Create a .env file in the project root and define the following variables:
```env
NODE_ENV=development
PORT=3000
JWT_SECRET=your-secure-secret-here
JWT_EXPIRES_IN=7d
```
### 3. Adding users
```bash
node addUser.js "user@email.com" "password"
```
### 4. Dev
```bash
pnpm dev
```
### 5. Production
```
pnpm start
````

62
addUser.js Normal file

@ -0,0 +1,62 @@
require('dotenv').config();
const bcrypt = require('bcrypt');
const { nanoid } = require('nanoid');
const { initializeDb, getDb } = require('./db/connect');
async function addUser(email, password) {
try {
await initializeDb();
const db = getDb();
const hashedPassword = await bcrypt.hash(password, 10);
const userId = nanoid();
return new Promise((resolve, reject) => {
const query = `
INSERT INTO users (id, email, password)
VALUES (?, ?, ?)
`;
db.run(query, [userId, email, hashedPassword], function(err) {
if (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
reject(new Error('Email already exists'));
} else {
reject(err);
}
} else {
resolve({
id: userId,
email: email
});
}
});
});
} catch (error) {
throw error;
}
}
if (require.main === module) {
const email = process.argv[2];
const password = process.argv[3];
if (!email || !password) {
console.error('Usage: node addUser.js <email> <password>');
process.exit(1);
}
addUser(email, password)
.then(user => {
console.log('User added successfully:', user);
console.log('You can now login with:');
console.log('Email:', email);
console.log('Password:', password);
process.exit(0);
})
.catch(error => {
console.error('Error adding user:', error.message);
process.exit(1);
});
}

BIN
bincode.db Normal file

Binary file not shown.

23
config/database.js Normal file

@ -0,0 +1,23 @@
require('dotenv').config();
const path = require('path');
module.exports = {
development: {
client: 'sqlite3',
connection: {
filename: './bincode.db'
},
useNullAsDefault: true
},
production: {
client: 'sqlite3',
connection: {
filename: process.env.DB_FILE || path.join(__dirname, '..', 'bincode.db')
},
useNullAsDefault: true,
pool: {
min: 2,
max: 10
}
}
};

7
config/index.js Normal file

@ -0,0 +1,7 @@
const database = require('./database');
const jwt = require('./jwt');
module.exports = {
database,
jwt
};

8
config/jwt.js Normal file

@ -0,0 +1,8 @@
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required');
}
module.exports = {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
};

95
db/connect.js Normal file

@ -0,0 +1,95 @@
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const config = require('../config/database');
let db;
const createTables = (db) => {
return new Promise((resolve, reject) => {
db.serialize(() => {
// enable foreign keys
db.run('PRAGMA foreign_keys = ON');
// users table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE,
password TEXT
)
`, (err) => {
if (err) {
console.error('Error creating users table:', err);
reject(err);
}
});
// snippets table
db.run(`
CREATE TABLE IF NOT EXISTS snippets (
id TEXT PRIMARY KEY,
user_id TEXT,
title TEXT,
html TEXT,
css TEXT,
js TEXT,
share_id TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
)
`, (err) => {
if (err) {
console.error('Error creating snippets table:', err);
reject(err);
}
resolve();
});
});
});
};
const initializeDb = async () => {
return new Promise((resolve, reject) => {
const env = process.env.NODE_ENV || 'development';
const dbPath = config[env].connection.filename;
const dbExists = fs.existsSync(dbPath);
if (db) {
db.close();
}
db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, async (err) => {
if (err) {
console.error('Database connection error:', err);
reject(err);
return;
}
try {
// If database doesn't exist, create tables
if (!dbExists) {
await createTables(db);
console.log('Database tables created successfully');
}
console.log('Database connection established');
resolve(db);
} catch (error) {
reject(error);
}
});
});
};
const getDb = () => {
if (!db) {
throw new Error('Database not initialized. Call initializeDb first.');
}
return db;
};
module.exports = {
initializeDb,
getDb
};

33
db/queries.js Normal file

@ -0,0 +1,33 @@
const snippetQueries = {
insert: `
INSERT INTO snippets (id, user_id, title, html, css, js, share_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`,
getUserSnippets: `
SELECT * FROM snippets
WHERE user_id = ?
ORDER BY created_at DESC
`,
getByShareId: `
SELECT html, css, js, title
FROM snippets
WHERE share_id = ?
`
};
const userQueries = {
findByEmail: `
SELECT * FROM users
WHERE email = ?
`,
insert: `
INSERT INTO users (id, email, password)
VALUES (?, ?, ?)
`
};
module.exports = {
snippets: snippetQueries,
users: userQueries
};

20
middleware/auth.js Normal file

@ -0,0 +1,20 @@
const jwt = require('jsonwebtoken');
const config = require('../config/jwt');
const auth = (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
const decoded = jwt.verify(token, config.secret);
req.userId = decoded.userId;
next();
} catch (error) {
console.error('Auth error:', error);
res.status(401).json({ error: 'Invalid token' });
}
};
module.exports = auth;

24
middleware/error.js Normal file

@ -0,0 +1,24 @@
const errorHandler = (err, req, res, next) => {
console.error({
error: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
timestamp: new Date().toISOString(),
path: req.path,
method: req.method
});
if (err.type === 'validation') {
return res.status(400).json({
error: 'Validation Error',
message: err.message
});
}
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message
});
};
module.exports = errorHandler;

9
middleware/index.js Normal file

@ -0,0 +1,9 @@
const auth = require('./auth');
const errorHandler = require('./error');
const { validateSnippet } = require('./validation');
module.exports = {
auth,
errorHandler,
validateSnippet
};

50
middleware/validation.js Normal file

@ -0,0 +1,50 @@
const validator = require('validator');
const xss = require('xss');
// auth validation
const validateAuth = (req, res, next) => {
const { email, password } = req.body;
// email validation
if (!email || !validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
// password validation
if (!password || password.length < 4 || password.length > 100) {
return res.status(400).json({ error: 'Invalid password format' });
}
// sanitize email
req.body.email = validator.normalizeEmail(email.toLowerCase().trim());
next();
};
// snippet validation
const validateSnippet = (req, res, next) => {
const { title } = req.body;
// title is required
if (!title || typeof title !== 'string' || title.length > 200) {
return res.status(400).json({ error: 'Invalid title' });
}
// sanitize title against XSS
req.body.title = xss(title.trim());
// enforce size limits on code content
const MAX_SIZE = 500 * 1024; // 500KB
if ((req.body.html?.length || 0) +
(req.body.css?.length || 0) +
(req.body.js?.length || 0) > MAX_SIZE) {
return res.status(400).json({ error: 'Content exceeds size limit' });
}
next();
};
module.exports = {
validateAuth,
validateSnippet
};

25
package.json Normal file

@ -0,0 +1,25 @@
{
"name": "bincode",
"version": "1.0.0",
"description": "A simple self-hosted code playground",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"bcrypt": "^5.1.0",
"dotenv": "^16.4.7",
"express": "^4.18.2",
"express-rate-limit": "^7.5.0",
"jsonwebtoken": "^9.0.0",
"nanoid": "^3.3.4",
"sanitize-html": "^2.14.0",
"sqlite3": "^5.1.6",
"validator": "^13.12.0",
"xss": "^1.0.15"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}

2168
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

329
public/app.js Normal file

@ -0,0 +1,329 @@
const { createApp } = Vue;
Prism.manual = true;
createApp({
data() {
return {
token: localStorage.getItem('token'),
showLogin: false,
showShareModal: false,
loginEmail: '',
loginPassword: '',
title: '',
html: '',
css: '',
js: '',
shareUrl: '',
currentShareId: null,
isDragging: false,
startX: null,
startWidth: null,
containerWidth: null,
editorWidth: '50%',
minWidth: 250,
maxWidth: null,
// Tab state
activeTab: 'html',
tabs: [
{ id: 'html', label: 'HTML', icon: '📄', language: 'markup' },
{ id: 'css', label: 'CSS', icon: '🎨', language: 'css' },
{ id: 'js', label: 'JavaScript', icon: '⚡', language: 'javascript' }
],
highlightedCode: {
html: '',
css: '',
js: ''
},
showEditor: true,
showPreview: true
};
},
computed: {
currentCode: {
get() {
return this[this.activeTab];
},
set(value) {
this[this.activeTab] = value;
this.highlightCode(value, this.activeTab);
}
},
previewWidth() {
if (typeof this.editorWidth === 'string' && this.editorWidth.endsWith('%')) {
return (100 - parseInt(this.editorWidth)) + '%';
}
return `calc(100% - ${this.editorWidth}px)`;
},
currentLanguage() {
const tab = this.tabs.find(t => t.id === this.activeTab);
return tab ? tab.language : 'markup';
}
},
watch: {
html(newVal) {
this.updatePreviewDebounced();
},
css(newVal) {
this.updatePreviewDebounced();
},
js(newVal) {
this.updatePreviewDebounced();
}
},
created() {
this.updatePreviewDebounced = this.debounce(this.updatePreview, 300);
const urlParams = new URLSearchParams(window.location.search);
const shareId = urlParams.get('share');
if (shareId) {
this.loadSharedSnippet(shareId);
}
},
mounted() {
this.initializeLayout();
document.addEventListener('keydown', this.handleKeyboardShortcuts);
// Initialize syntax highlighting
this.highlightCode(this.html, 'html');
this.highlightCode(this.css, 'css');
this.highlightCode(this.js, 'js');
// ensure iframe isloaded before updating the preview
const preview = document.getElementById('preview-frame');
if (preview) {
preview.onload = () => {
this.updatePreview();
}
}
},
methods: {
toggleEditor() {
this.showEditor = !this.showEditor;
},
togglePreview() {
this.showPreview = !this.showPreview;
if (!this.showPreview) { this.editorWidth = '100%';}
else { this.editorWidth = '50%'; }
},
highlightCode(code, tab) {
const languageMap = {
html: 'markup',
css: 'css',
js: 'javascript'
};
const language = languageMap[tab];
if (!language) return;
try {
this.highlightedCode[tab] = Prism.highlight(
code || '',
Prism.languages[language],
language
);
} catch (error) {
console.error('Highlighting error:', error);
this.highlightedCode[tab] = code || '';
}
},
handleKeydown(event) {
if (event.key === 'Tab') {
event.preventDefault();
const textarea = event.target;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const spaces = ' ';
const value = this.currentCode;
const beforeCursor = value.substring(0, start);
const afterCursor = value.substring(end);
if (event.shiftKey) {
// TODO: Shift + Tab undo 2 space
} else {
// Handle Tab (indent)
this.currentCode = beforeCursor + spaces + afterCursor;
this.$nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + spaces.length;
});
}
}
},
handleInput(event) {
const value = event.target.value;
this[this.activeTab] = value;
this.highlightCode(value, this.activeTab);
},
debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
},
handleKeyboardShortcuts(e) {
// Handle Ctrl/Cmd + number for tab switching
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
const num = parseInt(e.key);
if (num >= 1 && num <= this.tabs.length) {
this.activeTab = this.tabs[num - 1].id;
e.preventDefault();
}
}
// Handle Ctrl/Cmd + S for save
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
e.preventDefault();
this.saveSnippet();
}
},
initializeLayout() {
const container = document.querySelector('.editor-container');
this.containerWidth = container.offsetWidth;
this.maxWidth = this.containerWidth - this.minWidth;
this.updateLayout();
},
updateLayout() {
const editorGroup = document.querySelector('.editor-group');
const preview = document.querySelector('.preview');
if (editorGroup && preview) {
editorGroup.style.width = this.editorWidth;
preview.style.width = this.previewWidth;
}
},
updatePreview() {
const preview = document.getElementById('preview-frame');
if (!preview) return;
const doc = preview.contentDocument;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>${this.css}</style>
</head>
<body>
${this.html}
<script>${this.js}<\/script>
</body>
</html>
`);
doc.close();
},
switchTab(tabId) {
this.activeTab = tabId;
// re-highlight code when switching tabs
this.$nextTick(() => {
this.highlightCode(this[tabId], tabId);
const editor = document.querySelector('.editor-content textarea');
if (editor) editor.focus();
});
},
async login() {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: this.loginEmail,
password: this.loginPassword
})
});
if (!response.ok) throw new Error('Login failed');
const data = await response.json();
this.token = data.token;
localStorage.setItem('token', data.token);
this.showLogin = false;
} catch (error) {
alert('Login failed');
}
},
logout() {
this.token = null;
localStorage.removeItem('token');
},
async saveSnippet() {
if (!this.token) {
this.showLogin = true;
return;
}
try {
const response = await fetch('/api/snippets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
title: this.title || 'Untitled',
html: this.html,
css: this.css,
js: this.js
})
});
if (!response.ok) throw new Error('Failed to save snippet');
const data = await response.json();
this.currentShareId = data.shareId;
this.shareUrl = `${window.location.origin}/?share=${data.shareId}`;
this.showShareModal = true;
} catch (error) {
alert('Failed to save snippet');
}
},
async loadSharedSnippet(shareId) {
try {
const response = await fetch(`/api/snippets/share/${shareId}`);
if (!response.ok) throw new Error('Failed to load snippet');
const data = await response.json();
this.title = data.title;
this.html = data.html;
this.css = data.css;
this.js = data.js;
this.highlightCode(this.html, 'html');
this.highlightCode(this.css, 'css');
this.highlightCode(this.js, 'js');
this.updatePreview();
} catch (error) {
alert('Failed to load shared snippet');
}
},
async copyShareUrl() {
try {
await navigator.clipboard.writeText(this.shareUrl);
} catch (error) {
alert('Failed to copy URL');
}
}
}
}).mount('#app');

138
public/index.html Normal file

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JS Playground</title>
<link rel="stylesheet" href="/style.css" />
<!-- Prismjs css -->
<link rel="stylesheet" href="./libraries/prism/theme/prism.min.css" />
</head>
<body>
<div id="app">
<nav>
<div class="brand">BinCode</div>
<div class="title-input" v-if="token">
<input type="text" v-model="title" placeholder="Snippet title" />
</div>
<div>
<button class="toggle-icon" @click="toggleEditor">
{{ showEditor ? '🙈 editor': '👀 editor'}}
</button>
<button class="toggle-icon" @click="togglePreview">
{{ showPreview ? '🙈 preview' : '👀 preview' }}
</button>
</div>
<div class="nav-actions">
<div class="auth-buttons" v-if="!token">
<button @click="showLogin = true">Login</button>
</div>
<div class="user-menu" v-else>
<button @click="saveSnippet" class="primary">Save</button>
<button @click="logout">Logout</button>
</div>
</div>
</nav>
<div class="editor-container">
<div
class="editor-group"
v-show="showEditor"
:style="{ width: editorWidth }"
>
<!-- Tab bar -->
<div class="tab-bar">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab"
:class="{ active: activeTab === tab.id }"
@click="switchTab(tab.id)"
>
<span class="tab-icon">{{ tab.icon }}</span>
{{ tab.label }}
<kbd v-if="tab.id === 'html'">⌘1</kbd>
<kbd v-if="tab.id === 'css'">⌘2</kbd>
<kbd v-if="tab.id === 'js'">⌘3</kbd>
</button>
</div>
<!-- Editor content -->
<div class="editor-content">
<textarea
:value="currentCode"
@input="handleInput"
@keydown="handleKeydown"
:class="{ 'active-editor': true }"
spellcheck="false"
autocomplete="off"
:placeholder="'Enter your ' + activeTab.toUpperCase() + ' code here...'"
></textarea>
<pre
class="editor-highlight"
aria-hidden="true"
><code v-html="highlightedCode[activeTab]"></code></pre>
</div>
</div>
<div
class="preview"
:style="{ width: previewWidth }"
v-show="showPreview"
>
<iframe id="preview-frame"></iframe>
</div>
</div>
<!-- Share Modal -->
<div
class="modal"
v-if="showShareModal"
@click.self="showShareModal = false"
>
<div class="modal-content">
<h2>Share Snippet</h2>
<div class="share-url">
<input type="text" :value="shareUrl" readonly ref="shareUrlInput" />
<button @click="copyShareUrl" class="primary">Copy</button>
</div>
<button class="close" @click="showShareModal = false">&times;</button>
</div>
</div>
<!-- Login Modal -->
<div class="modal" v-if="showLogin" @click.self="showLogin = false">
<div class="modal-content">
<h2>Login</h2>
<form @submit.prevent="login">
<input
type="email"
v-model="loginEmail"
placeholder="Email"
required
/>
<input
type="password"
v-model="loginPassword"
placeholder="Password"
required
/>
<button type="submit" class="primary">Login</button>
</form>
<button class="close" @click="showLogin = false">&times;</button>
</div>
</div>
</div>
<!-- use`_dev` or `_prod` lib for vuejs based on env -->
<script src="./libraries/vue/vuejs_3.5.13_prod.js"></script>
<!-- Prismjs -->
<script src="./libraries/prism/prism.min.js"></script>
<script src="./libraries/prism/prism-markup.min.js"></script>
<script src="./libraries/prism/prism-css.min.js"></script>
<script src="./libraries/prism/prism-javascript.min.js"></script>
<script src="/app.js"></script>
</body>
</html>

@ -0,0 +1 @@
!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);

@ -0,0 +1 @@
Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;

@ -0,0 +1 @@
Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\[CDATA\[|\]\]>$/i;var t={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:<!\\[CDATA\\[(?:[^\\]]|\\](?!\\]>))*\\]\\]>|(?!<!\\[CDATA\\[)[^])*?(?=</__>)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;

1
public/libraries/prism/prism.min.js vendored Normal file

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

567
public/style.css Normal file

@ -0,0 +1,567 @@
:root {
--primary: #63DEAB;
--primary-dark: #48de9f;
--primary-light: rgba(26, 115, 232, 0.1);
--border: #e1e4e8;
--text: #000000;
--text-light: #6a737d;
--background: #ffffff;
--editor-bg: #fafbfc;
--tab-bg: #f6f8fa;
--tab-active: #ffffff;
--danger: #d73a49;
--success: #28a745;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--text);
line-height: 1.5;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
/* Navbar */
nav {
background: var(--tab-bg);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
}
.brand {
font-weight: 600;
font-size: 1.1rem;
color: var(--text);
flex-shrink: 0;
}
.title-input {
flex: 1;
}
.title-input input {
width: 100%;
max-width: 300px;
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.9rem;
background: var(--background);
}
.title-input input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);
}
.nav-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Main container */
.editor-container {
flex: 1;
display: flex;
overflow: hidden;
background: var(--editor-bg);
}
/* Editor group */
.editor-group {
display: flex;
flex-direction: column;
min-width: 250px;
background: var(--background);
transition: width 0.2s ease;
}
/* Tab bar */
.tab-bar {
display: flex;
background: var(--tab-bg);
padding: 0 0.5rem;
border-bottom: 1px solid var(--border);
height: 40px;
align-items: flex-end;
user-select: none;
overflow-x: auto;
overflow-y: hidden;
}
.tab {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
color: var(--text-light);
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
margin-bottom: -1px;
border-radius: 6px 6px 0 0;
background: transparent;
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab:hover {
color: var(--text);
background: rgba(0, 0, 0, 0.02);
}
.tab.active {
background: var(--tab-active);
color: var(--text);
border-color: var(--border);
border-bottom-color: var(--tab-active);
font-weight: 500;
}
.tab-icon {
font-size: 1.1em;
line-height: 1;
}
/* Editor content */
.editor-content {
position: relative;
flex: 1;
overflow: hidden;
background: var(--background);
}
.editor-content textarea,
.editor-content pre {
box-sizing: border-box;
padding: 1rem;
margin: 0;
border: 0;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: auto;
white-space: pre;
font-family: Monaco, 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.editor-content textarea {
color: transparent;
background: transparent;
caret-color: black;
z-index: 2;
resize: none;
}
.editor-content pre {
z-index: 1;
pointer-events: none;
background: transparent;
}
.editor-content pre code {
display: block;
font-family: inherit;
}
textarea:focus {
outline: none;
}
textarea::placeholder {
color: var(--text-light);
opacity: 0.6;
}
/* Resize handle */
.resize-handle {
width: 6px;
background: var(--tab-bg);
cursor: col-resize;
position: relative;
z-index: 10;
flex-shrink: 0;
}
.resize-handle::after {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
transform: translateX(-50%);
transition: background-color 0.2s;
}
body.resizing {
cursor: col-resize !important;
user-select: none;
}
body.resizing * {
cursor: col-resize !important;
}
body.resizing .resize-handle {
background: var(--primary-light);
}
body.resizing .resize-handle::after {
background: var(--primary);
}
/* Preview panel */
.preview {
flex: 1;
min-width: 250px;
background: var(--background);
border-left: 1px solid var(--border);
transition: width 0.2s ease;
}
iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
/* Keyboard shortcuts */
kbd {
padding: 0.1rem 0.4rem;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: 4px;
background: var(--background);
color: var(--text-light);
margin-left: 0.5rem;
}
/* Buttons */
button {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--background);
color: var(--text);
font-size: 0.9rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
}
button:hover {
background: var(--tab-bg);
}
button.primary {
background: var(--primary);
border-color: var(--primary);
color: white;
}
button.primary:hover {
background: var(--primary-dark);
border-color: var(--primary-dark);
}
/* Modals */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
form {
padding: 1rem;
margin-bottom: 0.5rem;
}
form > input {
margin:0.5rem auto;
}
form > button {
margin-top: 0.25rem;
}
.modal-content {
background: var(--background);
border-radius: 8px;
padding: 1.5rem;
width: 90%;
max-width: 400px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
position: relative;
display: flex;
}
.modal h2 {
margin-bottom: 0;
font-size: 1.25rem;
font-weight: 600;
}
.modal input:not([type="submit"]) {
width: 100%;
padding: 0.6rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 0;
font-size: 0.9rem;
}
.modal input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);
}
.close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-light);
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.close:hover {
color: var(--text);
background: none;
}
/* Share URL */
.share-url {
display: flex;
gap: 0.5rem;
align-items: center;
flex-direction: row;
padding: 1rem;
}
.share-url > button {
height: 40px;
padding: 0.6rem 0.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--primary);
color: white;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.share-url input {
margin-bottom: 0;
flex: 1;
}
/* View toggles */
.view-toggles {
display: flex;
gap: 0.5rem;
margin-right: 1rem;
}
.view-toggles button {
background: var(--tab-bg);
border: 1px solid var(--border);
padding: 0.4rem 0.75rem;
font-size: 0.9rem;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-light);
transition: all 0.2s;
}
.view-toggles button:hover {
background: var(--background);
color: var(--text);
}
.view-toggles button.active {
background: var(--background);
color: var(--primary);
border-color: var(--primary);
}
.icon {
font-size: 1.1em;
line-height: 1;
}
/* Main container */
.editor-container {
flex: 1;
display: flex;
overflow: hidden;
background: var(--editor-bg);
position: relative;
}
/* Editor group */
.editor-group {
display: flex;
flex-direction: column;
background: var(--background);
transition: all 0.3s ease;
width: 100%;
}
.editor-group.hidden {
display: none;
}
/* Preview panel */
.preview {
flex: 1;
background: var(--background);
border-left: 1px solid var(--border);
transition: all 0.3s ease;
}
.preview.hidden {
display: none;
}
.preview.full-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
border: none;
}
.user-menu > button {
margin-left: 10px;
}
.toggle-icon {
background: transparent;
border-width: 1px;
border:rgba(0, 0, 0, 0.02);
cursor: pointer;
font-size: 1rem;
padding: 5px 10px;
color: #555;
transition: color 0.3s;
}
.toggle-icon:hover {
color: #000;
}
/* Responsive */
@media (max-width: 768px) {
.user-menu {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
gap: 4px;
}
button {
padding: 0.25rem 0.30rem;
gap: 0;
}
.user-menu > button {
margin-left: 0;
}
.editor-container {
flex-direction: column;
}
.editor-group,
.preview {
width: 100% !important;
height: 50%;
min-width: 0;
}
.resize-handle {
display: none;
}
.title-input {
display: none;
}
}
@media (prefers-color-scheme: dark) {
:root {
--primary: #63DEAB;
--primary-dark: #63DEAB;
--primary-light: rgba(77, 159, 255, 0.1);
--border: #30363d;
--text: #ffffff;
--text-light: #8b949e;
--background: #0d1117;
--editor-bg: #161b22;
--tab-bg: #21262d;
--tab-active: #0d1117;
}
input, .toggle-icon {
color: white;
}
::selection {
background: var(--primary-light);
color: var(--text);
}
}

46
routes/auth.js Normal file

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { getDb } = require('../db/connect');
const { users: userQueries } = require('../db/queries');
const config = require('../config');
const { validateAuth } = require('../middleware/validation');
router.post('/login', validateAuth, async (req, res, next) => {
try {
const { email, password } = req.body;
const db = getDb();
db.get(userQueries.findByEmail, [email], async (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
console.log('User found:', { id: user.id, email: user.email });
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id },
config.jwt.secret,
{ expiresIn: config.jwt.expiresIn }
);
res.json({ token });
});
} catch (error) {
next(error);
}
});
module.exports = router;

9
routes/index.js Normal file

@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const authRoutes = require('./auth');
const snippetRoutes = require('./snippets');
router.use('/auth', authRoutes);
router.use('/snippets', snippetRoutes);
module.exports = router;

52
routes/snippets.js Normal file

@ -0,0 +1,52 @@
const express = require('express');
const router = express.Router();
const { nanoid } = require('nanoid');
const auth = require('../middleware/auth');
const { getDb } = require('../db/connect');
const { validateSnippet } = require('../middleware/validation');
const { snippets: snippetQueries } = require('../db/queries');
router.post('/', auth, validateSnippet, async (req, res) => {
const { title, html, css, js } = req.body;
const snippetId = nanoid();
const shareId = nanoid(10);
const db = getDb();
db.run(
snippetQueries.insert,
[snippetId, req.userId, title, html, css, js, shareId],
(err) => {
if (err) {
console.error('Save snippet error:', err);
return res.status(500).json({ error: 'Error saving snippet' });
}
res.json({ id: snippetId, shareId });
}
);
});
router.get('/share/:shareId', async (req, res) => {
const { shareId } = req.params;
const db = getDb();
console.log(shareId);
db.get(
snippetQueries.getByShareId,
[shareId],
(err, row) => {
if (err) {
console.error('Error fetching snippet:', err);
return res.status(500).json({ error: 'Error fetching snippet' });
}
if (!row) {
return res.status(404).json({ error: 'Snippet not found' });
}
res.json(row);
}
);
});
module.exports = router;

41
server.js Normal file

@ -0,0 +1,41 @@
require('dotenv').config();
const express = require('express');
const rateLimit = require('express-rate-limit');
const { initializeDb } = require('./db/connect');
const routes = require('./routes');
const app = express();
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}));
app.use(express.json());
app.use(express.static('public'));
// routes
app.use('/api', routes);
// error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something broke!' });
});
const PORT = process.env.PORT || 3000;
const startServer = async () => {
try {
await initializeDb();
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
startServer();

27
services/auth.js Normal file

@ -0,0 +1,27 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { getDb } = require('../db/connect');
const { users: userQueries } = require('../db/queries');
const config = require('../config');
const hashPassword = async (password) => {
return bcrypt.hash(password, 10);
};
const verifyPassword = async (password, hash) => {
return bcrypt.compare(password, hash);
};
const generateToken = (userId) => {
return jwt.sign(
{ userId },
config.jwt.secret,
{ expiresIn: config.jwt.expiresIn }
);
};
module.exports = {
hashPassword,
verifyPassword,
generateToken
};

48
services/snippets.js Normal file

@ -0,0 +1,48 @@
const { nanoid } = require('nanoid');
const { getDb } = require('../db/connect');
const { snippets: snippetQueries } = require('../db/queries');
const createSnippet = async (userId, { title, html, css, js }) => {
const snippetId = nanoid();
const shareId = nanoid(10);
const db = getDb();
return new Promise((resolve, reject) => {
db.run(
snippetQueries.insert,
[snippetId, userId, title, html, css, js, shareId],
function(err) {
if (err) reject(err);
resolve({ id: snippetId, shareId });
}
);
});
};
const getUserSnippets = async (userId) => {
const db = getDb();
return new Promise((resolve, reject) => {
db.all(snippetQueries.getUserSnippets, [userId], (err, snippets) => {
if (err) reject(err);
resolve(snippets);
});
});
};
const getSharedSnippet = async (shareId) => {
const db = getDb();
return new Promise((resolve, reject) => {
db.get(snippetQueries.getByShareId, [shareId], (err, snippet) => {
if (err) reject(err);
resolve(snippet);
});
});
};
module.exports = {
createSnippet,
getUserSnippets,
getSharedSnippet
};

26
utils/logger.js Normal file

@ -0,0 +1,26 @@
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.simple()
})
]
});
if (process.env.NODE_ENV === 'production') {
logger.add(new winston.transports.File({
filename: 'error.log',
level: 'error'
}));
logger.add(new winston.transports.File({
filename: 'combined.log'
}));
}
module.exports = logger;

22
utils/security.js Normal file

@ -0,0 +1,22 @@
const crypto = require('crypto');
const generateSecureId = (length = 21) => {
return crypto
.randomBytes(Math.ceil(length * 3 / 4))
.toString('base64')
.slice(0, length)
.replace(/\+/g, '-')
.replace(/\//g, '_');
};
const sanitizeInput = (input) => {
if (typeof input !== 'string') return input;
return input
.replace(/[<>]/g, '')
.trim();
};
module.exports = {
generateSecureId,
sanitizeInput
};