init
This commit is contained in:
commit
9fb7bd46e0
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
|
||||
playground.db
|
||||
|
||||
.env
|
70
README.md
Normal file
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
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
BIN
bincode.db
Normal file
Binary file not shown.
23
config/database.js
Normal file
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
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
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
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
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
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
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
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
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
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
2168
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
329
public/app.js
Normal file
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
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">×</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">×</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>
|
1
public/libraries/prism/prism-css.min.js
vendored
Normal file
1
public/libraries/prism/prism-css.min.js
vendored
Normal file
@ -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);
|
1
public/libraries/prism/prism-javascript.min.js
vendored
Normal file
1
public/libraries/prism/prism-javascript.min.js
vendored
Normal file
@ -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;
|
1
public/libraries/prism/prism-markup.min.js
vendored
Normal file
1
public/libraries/prism/prism-markup.min.js
vendored
Normal file
@ -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(/&/,"&"))})),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
1
public/libraries/prism/prism.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/libraries/prism/theme/prism.min.css
vendored
Normal file
1
public/libraries/prism/theme/prism.min.css
vendored
Normal file
@ -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}
|
13
public/libraries/vue/vuejs_3.5.13_dev.js
Normal file
13
public/libraries/vue/vuejs_3.5.13_dev.js
Normal file
File diff suppressed because one or more lines are too long
9
public/libraries/vue/vuejs_3.5.13_prod.js
Normal file
9
public/libraries/vue/vuejs_3.5.13_prod.js
Normal file
File diff suppressed because one or more lines are too long
567
public/style.css
Normal file
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
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
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
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
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
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
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
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
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
|
||||
};
|
Loading…
Reference in New Issue
Block a user