├── .gitignore ├── src ├── vite-env.d.ts ├── database.ts ├── server │ ├── models │ │ ├── index.ts │ │ ├── Snippet.ts │ │ └── User.ts │ ├── routes │ │ ├── index.ts │ │ ├── static.ts │ │ ├── admin.ts │ │ ├── auth.ts │ │ └── snippets.ts │ └── index.ts ├── main.tsx ├── config.ts ├── types.ts ├── App.css ├── index.css ├── App.tsx ├── components │ ├── Register.tsx │ ├── Login.tsx │ ├── SnippetList.tsx │ └── Editor.tsx ├── styles.ts └── assets │ └── react.svg ├── docker-compose.yml ├── tsconfig.node.json ├── tsconfig.server.json ├── index.html ├── vite.config.ts ├── tsconfig.json ├── Dockerfile ├── tsconfig.app.json ├── eslint.config.js ├── public └── vite.svg ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | database.sqlite 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import Database from 'better-sqlite3'; 2 | 3 | const DB_PATH = './database.sqlite'; 4 | const db = new Database(DB_PATH); // This opens the DB synchronously 5 | 6 | export default db; 7 | -------------------------------------------------------------------------------- /src/server/models/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | 3 | export const sequelize = new Sequelize({ 4 | dialect: 'sqlite', 5 | storage: './database.sqlite', 6 | logging: false 7 | }); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | ports: 6 | - "3000:3000" 7 | volumes: 8 | - ./data:/app/data 9 | environment: 10 | - NODE_ENV=production 11 | - PORT=3000 -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL = ''; // Empty string for same-origin requests 2 | export const API_ENDPOINTS = { 3 | LOGIN: `/api/auth/login`, 4 | REGISTER: `/api/auth/register`, 5 | SNIPPETS: `/api/snippets`, 6 | SNIPPET: (id: string) => `/api/snippets/${id}`, 7 | }; 8 | export const JWT_SECRET_KEY = 'jwt-secret-key'; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Snippet { 2 | id?: string; 3 | title: string; 4 | content: string; 5 | language: string; 6 | userId: string; // Added to track snippet ownership 7 | } 8 | 9 | export interface User { 10 | username: string; 11 | token: string; 12 | } 13 | 14 | export enum Role { 15 | USER, 16 | ADMIN, 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/server/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CodeSignal Learn Pastebin 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | host: '0.0.0.0', 8 | port: 3000, 9 | allowedHosts: true, 10 | proxy: { 11 | '/api': { 12 | target: 'http://localhost:3001', 13 | changeOrigin: true 14 | } 15 | } 16 | }, 17 | build: { 18 | outDir: 'dist/client' 19 | } 20 | }); -------------------------------------------------------------------------------- /src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import authRoutes from './auth'; 3 | import snippetRoutes from './snippets'; 4 | import adminRoutes from './admin'; 5 | import staticRoutes from './static'; 6 | 7 | const router = express.Router(); 8 | 9 | router.use('/api', express.json()); 10 | router.use('/api/auth', authRoutes); 11 | router.use('/api/snippets', snippetRoutes); 12 | router.use('/api/admin', adminRoutes); 13 | router.use('/', staticRoutes); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /src/server/routes/static.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { join } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { dirname } from 'path'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | const router = express.Router(); 10 | 11 | // Catch-all route to serve index.html for client-side routing 12 | router.get('*', (_, res) => { 13 | res.sendFile(join(__dirname, '../../../dist/client/index.html')); 14 | }); 15 | 16 | export default router; -------------------------------------------------------------------------------- /src/server/models/Snippet.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from './index.js'; 3 | 4 | export class Snippet extends Model { 5 | declare id: string; 6 | declare title: string; 7 | declare content: string; 8 | declare language: string; 9 | declare userId: number; 10 | } 11 | 12 | Snippet.init({ 13 | id: { 14 | type: DataTypes.STRING, 15 | primaryKey: true 16 | }, 17 | title: DataTypes.STRING, 18 | content: DataTypes.TEXT, 19 | language: DataTypes.STRING, 20 | userId: DataTypes.INTEGER 21 | }, { 22 | sequelize, 23 | modelName: 'Snippet' 24 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "allowImportingTsExtensions": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "Node" // This is necessary to avoid IDE warnings 19 | }, 20 | "include": ["src"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } -------------------------------------------------------------------------------- /src/server/models/User.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from './index.js'; 3 | import { Role } from '../../types.js'; 4 | 5 | export class User extends Model { 6 | declare id: number; 7 | declare username: string; 8 | declare password: string; 9 | declare role: string; // new field for user role 10 | } 11 | 12 | User.init({ 13 | id: { 14 | type: DataTypes.INTEGER, 15 | primaryKey: true, 16 | autoIncrement: true 17 | }, 18 | username: DataTypes.STRING, 19 | password: DataTypes.STRING, 20 | role: { 21 | type: DataTypes.ENUM(Role.USER.toString(), Role.ADMIN.toString()), 22 | defaultValue: Role.USER.toString(), 23 | } 24 | }, { 25 | sequelize, 26 | modelName: 'User' 27 | }); 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js as base image 2 | FROM node:20-slim 3 | 4 | # Install build dependencies for sqlite3 5 | RUN apt-get update 6 | RUN apt-get install -y \ 7 | python3 \ 8 | make \ 9 | g++ \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | # Set working directory 13 | WORKDIR /app 14 | 15 | # Copy package files 16 | COPY package*.json ./ 17 | 18 | # Install dependencies 19 | RUN npm install 20 | 21 | # Copy source code 22 | COPY . . 23 | 24 | # Build the frontend 25 | RUN npm run build 26 | 27 | # Build the server 28 | RUN npm run build:server 29 | 30 | # Rebuild sqlite3 from source 31 | RUN npm rebuild sqlite3 --build-from-source 32 | 33 | # Expose port 34 | EXPOSE 3000 35 | 36 | # Start the server 37 | CMD ["npm", "run", "start"] 38 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import { fileURLToPath } from 'url'; 4 | import { dirname, join } from 'path'; 5 | import bcrypt from 'bcryptjs'; 6 | import { User } from './models/User.js'; 7 | import { sequelize } from './models/index.js'; 8 | import routes from './routes/index.js'; 9 | import { Role } from '../types'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | const app = express(); 15 | app.use(cors()); 16 | 17 | // Serve static files from the dist/client directory 18 | app.use(express.static(join(__dirname, '../../dist/client'))); 19 | 20 | const PORT = process.env.PORT || 3001; 21 | 22 | // Initialize database 23 | sequelize.sync({ alter: true }).then(async () => { 24 | try { 25 | console.log("Database synchronized with alter: true."); 26 | // Create default user 27 | const hashedPassword = await bcrypt.hash('admin', 10); 28 | await User.findOrCreate({ 29 | where: { username: 'admin' }, 30 | defaults: { password: hashedPassword, role: Role.ADMIN.toString() } 31 | }); 32 | } catch (error) { 33 | console.error("Error initializing database:", error); 34 | } 35 | }); 36 | 37 | // Use routes 38 | app.use(routes); 39 | 40 | app.listen(PORT, () => { 41 | console.log(`Server running on port ${PORT}`); 42 | }); -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/server/routes/admin.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import { User } from '../models/User'; 4 | import { JWT_SECRET_KEY } from '../../config'; 5 | import { Role } from '../../types'; 6 | import db from '../../database'; 7 | 8 | const router = express.Router(); 9 | 10 | // Admin-only test endpoint 11 | router.get('/test', async (req, res) => { 12 | const authHeader = req.headers.authorization; 13 | if (!authHeader) { 14 | return res.status(401).json({ error: "Missing authorization header" }); 15 | } 16 | const token = authHeader.split(' ')[1]; 17 | try { 18 | const decoded: any = jwt.verify(token, JWT_SECRET_KEY); 19 | const user = await User.findByPk(decoded.userId); 20 | if (!user || user.role !== Role.ADMIN.toString()) { 21 | return res.status(403).json({ error: "Access denied" }); 22 | } 23 | res.json({ message: "Admin test endpoint accessed successfully" }); 24 | } catch (error) { 25 | console.error("Error verifying token:", error); 26 | res.status(401).json({ error: "Invalid token" }); 27 | } 28 | }); 29 | 30 | router.get('/testOpen', async (_req, res) => { 31 | res.json({ message: "Test endpoint is working!" }); 32 | }); 33 | 34 | // Secure account info endpoint 35 | router.get('/accountInfo', (req, res) => { 36 | const userId = req.query.id; 37 | if (!userId) { 38 | return res.status(400).json({ error: "Missing user ID parameter" }); 39 | } 40 | 41 | try { 42 | const query = `SELECT * FROM users WHERE id = ?`; 43 | // Prepare and execute the query synchronously 44 | const stmt = db.prepare(query); 45 | const results = stmt.all(userId); 46 | res.json(results); 47 | } catch (error) { 48 | console.error("Database query error:", error); 49 | res.status(500).json({ error: "Internal server error" }); 50 | } 51 | }); 52 | 53 | 54 | 55 | export default router; -------------------------------------------------------------------------------- /src/server/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import bcrypt from 'bcryptjs'; 4 | import { User } from '../models/User'; 5 | import { JWT_SECRET_KEY } from '../../config'; 6 | import { Role } from '../../types'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post('/register', async (req, res) => { 11 | const { username, password, role } = req.body; 12 | // Ensure role is either 'admin' or 'user'; default to 'user' 13 | const userRole = role === Role.ADMIN.toString() ? Role.ADMIN : Role.USER; 14 | try { 15 | const existingUser = await User.findOne({ where: { username } }); 16 | if (existingUser) { 17 | return res.status(400).json({ error: "Username already exists" }); 18 | } 19 | const hashedPassword = await bcrypt.hash(password, 10); 20 | const user = await User.create({ 21 | username, 22 | password: hashedPassword, 23 | role: userRole.toString(), 24 | }); 25 | res.json({ message: "User registered successfully", userId: user.id }); 26 | } catch (error) { 27 | console.error("Error during registration:", error); 28 | res.status(500).json({ error: "Internal server error" }); 29 | } 30 | }); 31 | 32 | // Login endpoint 33 | router.post('/login', async (req, res) => { 34 | try { 35 | const { username, password } = req.body; 36 | const user = await User.findOne({ where: { username } }); 37 | 38 | if (user && await bcrypt.compare(password, user.password)) { 39 | const token = jwt.sign({ userId: user.id }, JWT_SECRET_KEY); 40 | return res.json({ token }); 41 | } else { 42 | return res.status(401).json({ error: 'Invalid credentials' }); 43 | } 44 | } catch (error) { 45 | console.error("Error during login:", error); 46 | res.status(500).json({ error: 'Internal server error' }); 47 | } 48 | }); 49 | 50 | export default router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pastebin-demo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "server": "npm run build:server && npm run start:server", 12 | "build:server": "tsc --project tsconfig.server.json", 13 | "start:server": "node dist/server/index.js", 14 | "start": "npm run build && npm run build:server && npm run start:server", 15 | "dev:server": "tsx watch src/server/index.ts", 16 | "dev:all": "concurrently \"npm run dev\" \"npm run dev:server\"" 17 | }, 18 | "dependencies": { 19 | "@codemirror/lang-cpp": "6.0.2", 20 | "@codemirror/lang-java": "6.0.1", 21 | "@codemirror/lang-javascript": "6.2.1", 22 | "@codemirror/lang-python": "6.1.3", 23 | "@uiw/react-codemirror": "4.21.21", 24 | "bcryptjs": "2.4.3", 25 | "better-sqlite3": "^11.8.1", 26 | "cors": "2.8.5", 27 | "express": "4.18.2", 28 | "jsonwebtoken": "9.0.2", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-router-dom": "6.22.1", 32 | "sequelize": "6.37.1", 33 | "sqlite3": "5.1.7" 34 | }, 35 | "devDependencies": { 36 | "@types/bcryptjs": "2.4.6", 37 | "@types/better-sqlite3": "^7.6.12", 38 | "@types/cors": "2.8.17", 39 | "@types/express": "4.17.21", 40 | "@types/jsonwebtoken": "9.0.5", 41 | "@types/node": "20.17.17", 42 | "@types/react": "18.2.55", 43 | "@types/react-dom": "18.2.19", 44 | "@types/uuid": "^10.0.0", 45 | "@typescript-eslint/eslint-plugin": "6.21.0", 46 | "@typescript-eslint/parser": "6.21.0", 47 | "@vitejs/plugin-react": "4.2.1", 48 | "concurrently": "8.2.2", 49 | "eslint": "8.56.0", 50 | "eslint-plugin-react-hooks": "4.6.0", 51 | "eslint-plugin-react-refresh": "0.4.5", 52 | "ts-node": "10.9.2", 53 | "tsx": "4.7.0", 54 | "typescript": "5.7.3", 55 | "vite": "5.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 2 | import { useState, useEffect } from 'react'; 3 | import Editor from './components/Editor'; 4 | import Login from './components/Login'; 5 | import Register from './components/Register'; 6 | import SnippetList from './components/SnippetList'; 7 | import { User } from './types'; 8 | import { styles } from './styles'; 9 | 10 | function App() { 11 | const [user, setUser] = useState(() => { 12 | const savedUser = localStorage.getItem('user'); 13 | return savedUser ? JSON.parse(savedUser) : null; 14 | }); 15 | 16 | useEffect(() => { 17 | if (user) { 18 | localStorage.setItem('user', JSON.stringify(user)); 19 | } else { 20 | localStorage.removeItem('user'); 21 | } 22 | }, [user]); 23 | 24 | const handleLogout = () => { 25 | setUser(null); 26 | window.location.href = '/login'; 27 | }; 28 | 29 | return ( 30 | 31 |
32 |
33 |
34 |

CodeSignal Code Snippet Tool

35 |
36 | {user && ( 37 |
38 | 39 | 45 |
46 | )} 47 |
48 | 49 | {user ? ( 50 | <> 51 | } /> 52 | } /> 53 | 54 | ) : ( 55 | <> 56 | } /> 57 | } /> 58 | } /> 59 | 60 | )} 61 | 62 |
63 |
64 | ); 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /src/components/Register.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { styles } from '../styles'; 4 | import { API_ENDPOINTS } from '../config'; 5 | 6 | function Register() { 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const [role, setRole] = useState('user'); 10 | const [error, setError] = useState(''); 11 | const navigate = useNavigate(); 12 | 13 | const handleSubmit = async (e: React.FormEvent) => { 14 | e.preventDefault(); 15 | setError(''); 16 | try { 17 | const response = await fetch(API_ENDPOINTS.REGISTER, { 18 | method: 'POST', 19 | headers: { 'Content-Type': 'application/json' }, 20 | body: JSON.stringify({ username, password, role }), 21 | }); 22 | if (response.ok) { 23 | // Registration successful, navigate to login 24 | alert("Registration successful! Please log in."); 25 | navigate('/login'); 26 | } else { 27 | const data = await response.json(); 28 | setError(data.error || 'Registration failed'); 29 | } 30 | } catch (err) { 31 | setError('Connection error. Please try again.'); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 |

Register

38 |
39 | setUsername(e.target.value)} 45 | /> 46 | setPassword(e.target.value)} 52 | /> 53 | 61 | {error &&
{error}
} 62 | 63 |
64 |
65 | ); 66 | } 67 | 68 | export default Register; 69 | -------------------------------------------------------------------------------- /src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate, Link } from 'react-router-dom'; 3 | import { User } from '../types'; 4 | import { styles } from '../styles'; 5 | import { API_ENDPOINTS } from '../config'; 6 | 7 | interface LoginProps { 8 | onLogin: (user: User) => void; 9 | } 10 | 11 | function Login({ onLogin }: LoginProps) { 12 | const [username, setUsername] = useState(''); 13 | const [password, setPassword] = useState(''); 14 | const [error, setError] = useState(''); 15 | const navigate = useNavigate(); 16 | 17 | const handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | setError(''); 20 | 21 | try { 22 | const response = await fetch(API_ENDPOINTS.LOGIN, { 23 | method: 'POST', 24 | headers: { 'Content-Type': 'application/json' }, 25 | body: JSON.stringify({ username, password }), 26 | }); 27 | 28 | if (response.ok) { 29 | const { token } = await response.json(); 30 | onLogin({ username, token }); 31 | navigate("/"); 32 | } else { 33 | setError('Invalid credentials'); 34 | } 35 | } catch (err) { 36 | setError('Connection error. Please try again.'); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 |

Login to CodeSignal Code Snippet Tool

43 |
44 | setUsername(e.target.value)} 50 | /> 51 | setPassword(e.target.value)} 57 | /> 58 | {error &&
{error}
} 59 | 62 |
63 | {/* New Register button */} 64 |
65 | Don't have an account?{' '} 66 | 67 | Register 68 | 69 |
70 |
71 | ); 72 | } 73 | 74 | export default Login; 75 | -------------------------------------------------------------------------------- /src/components/SnippetList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { API_ENDPOINTS } from '../config'; 4 | import { Snippet } from '../types'; 5 | import { styles } from '../styles'; 6 | 7 | function SnippetList() { 8 | const [snippets, setSnippets] = useState([]); 9 | const navigate = useNavigate(); 10 | const location = useLocation(); 11 | 12 | // Extract current snippet id from URL if present 13 | const currentSnippetId = location.pathname.startsWith('/snippet/') 14 | ? location.pathname.split('/snippet/')[1] 15 | : ''; 16 | 17 | useEffect(() => { 18 | const storedUser = localStorage.getItem('user'); 19 | if (!storedUser) { 20 | console.error("No user found in localStorage"); 21 | return; 22 | } 23 | 24 | const token = JSON.parse(storedUser).token; 25 | if (!token) { 26 | console.error("No token found in user data"); 27 | return; 28 | } 29 | 30 | fetch(API_ENDPOINTS.SNIPPETS, { 31 | method: 'GET', 32 | headers: { 33 | 'Authorization': `Bearer ${token}`, 34 | 'Content-Type': 'application/json' 35 | }, 36 | }) 37 | .then(res => { 38 | if (!res.ok) { 39 | throw new Error(`Failed to fetch snippets: ${res.status}`); 40 | } 41 | return res.json(); 42 | }) 43 | .then(setSnippets) 44 | .catch(error => { 45 | console.error("Error fetching snippets:", error); 46 | // Optionally handle unauthorized errors here 47 | if (error.message.includes('401')) { 48 | // Handle unauthorized access - maybe redirect to login 49 | navigate('/login'); 50 | } 51 | }); 52 | }, [location, navigate]); 53 | 54 | 55 | const handleSelect = (e: React.ChangeEvent) => { 56 | const snippetId = e.target.value; 57 | if (snippetId) { 58 | navigate(`/snippet/${snippetId}`); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 | 65 | 79 |
80 | ); 81 | } 82 | 83 | export default SnippetList; 84 | -------------------------------------------------------------------------------- /src/server/routes/snippets.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { Snippet } from '../models/Snippet'; 5 | import { JWT_SECRET_KEY } from '../../config'; 6 | 7 | const router = express.Router(); 8 | 9 | // Save snippet endpoint 10 | router.post('/', async (req, res) => { 11 | const authHeader = req.headers.authorization; 12 | if (!authHeader) { 13 | return res.status(401).json({ error: "Missing authorization header" }); 14 | } 15 | const token = authHeader.split(' ')[1]; 16 | let decoded: any; 17 | try { 18 | decoded = jwt.verify(token, JWT_SECRET_KEY); 19 | } catch (error) { 20 | return res.status(401).json({ error: "Invalid token" }); 21 | } 22 | const userId = decoded.userId; 23 | try { 24 | const { title, content, language } = req.body; 25 | const snippet = await Snippet.create({ 26 | id: uuidv4(), 27 | title, 28 | content, 29 | language, 30 | userId, 31 | }); 32 | res.json(snippet); 33 | } catch (error) { 34 | console.error("Error saving snippet:", error); 35 | res.status(500).json({ error: "Failed to save snippet" }); 36 | } 37 | }); 38 | 39 | // Get snippet endpoint 40 | router.get('/:id', async (req, res) => { 41 | try { 42 | const snippet = await Snippet.findByPk(req.params.id); 43 | if (!snippet) { 44 | return res.status(404).json({ error: "Snippet not found" }); 45 | } 46 | res.json(snippet); 47 | } catch (error) { 48 | console.error("Error retrieving snippet:", error); 49 | res.status(500).json({ error: "Failed to retrieve snippet" }); 50 | } 51 | }); 52 | 53 | router.get('/', async (req, res) => { 54 | const authHeader = req.headers.authorization; 55 | if (!authHeader) { 56 | return res.status(401).json({ error: "Missing authorization header" }); 57 | } 58 | const token = authHeader.split(' ')[1]; 59 | let decoded: any; 60 | try { 61 | decoded = jwt.verify(token, JWT_SECRET_KEY); 62 | } catch (error) { 63 | return res.status(401).json({ error: "Invalid token" }); 64 | } 65 | const userId = decoded.userId; 66 | try { 67 | const snippets = await Snippet.findAll({ where: { userId } }); 68 | res.json(snippets); 69 | } catch (error) { 70 | console.error("Error retrieving snippets:", error); 71 | res.status(500).json({ error: "Failed to retrieve snippets" }); 72 | } 73 | }); 74 | 75 | // Delete snippet endpoint 76 | router.delete('/:id', async (req, res) => { 77 | try { 78 | const snippet = await Snippet.findByPk(req.params.id); 79 | if (!snippet) { 80 | return res.status(404).json({ error: "Snippet not found" }); 81 | } 82 | await snippet.destroy(); 83 | res.json({ message: "Snippet deleted successfully" }); 84 | } catch (error) { 85 | console.error("Error deleting snippet:", error); 86 | res.status(500).json({ error: "Failed to delete snippet" }); 87 | } 88 | }); 89 | 90 | export default router; 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pastebin Demo Application 2 | 3 | A simple Pastebin-like application built with React, TypeScript, and Express. This application is intentionally built with minimal security measures for educational purposes in security courses. 4 | 5 | ## Features 6 | 7 | - Code snippet creation and editing 8 | - Support for multiple programming languages: 9 | - TypeScript 10 | - JavaScript 11 | - Python 12 | - Java 13 | - C++ 14 | - Syntax highlighting using CodeMirror 15 | - File upload functionality 16 | - Basic user authentication 17 | - Unique URLs for each saved snippet 18 | - SQLite database with Sequelize ORM 19 | 20 | ## ⚠️ Security Notice 21 | 22 | This application is deliberately built WITHOUT security measures for educational purposes. It contains various vulnerabilities including but not limited to: 23 | - SQL Injection possibilities 24 | - No input validation 25 | - Weak authentication 26 | - No CSRF protection 27 | - Potential XSS vulnerabilities 28 | 29 | DO NOT USE THIS IN PRODUCTION! 30 | 31 | ## Prerequisites 32 | 33 | - Node.js (v14 or higher) 34 | - npm (Node Package Manager) 35 | 36 | ## Installation 37 | 38 | 1. Clone the repository: 39 | ```bash 40 | git clone [repository-url] 41 | cd learn_paste-bin 42 | ``` 43 | 44 | 2. Install dependencies: 45 | ```bash 46 | npm install 47 | ``` 48 | 49 | 3. Start the backend and frontend development server: 50 | ```bash 51 | npm run dev:all 52 | ``` 53 | 54 | ## Usage 55 | 56 | 1. Access the application at `http://localhost:3000` 57 | 58 | 2. Login with default credentials: 59 | - Username: `admin` 60 | - Password: `admin` 61 | 62 | 3. Create new snippets: 63 | - Enter a title 64 | - Select a programming language 65 | - Write or paste your code 66 | - Click "Save" to generate a unique URL 67 | 68 | 4. Upload files: 69 | - Click the file upload button 70 | - Select a text file 71 | - The content will be automatically loaded into the editor 72 | 73 | 5. Access saved snippets: 74 | - Use the generated URL (format: `/snippet/:id`) 75 | - Edit and save changes as needed 76 | 77 | ## Project Structure 78 | 79 | ``` 80 | src/ 81 | ├── server/ 82 | │ ├── models/ 83 | │ │ ├── index.ts 84 | │ │ ├── Snippet.ts 85 | │ │ └── User.ts 86 | │ └── index.ts 87 | ├── components/ 88 | │ ├── Editor.tsx 89 | │ └── Login.tsx 90 | ├── types.ts 91 | ├── App.tsx 92 | └── main.tsx 93 | ``` 94 | 95 | ## API Endpoints 96 | 97 | - `POST /api/login` - User authentication 98 | - `POST /api/snippets` - Create/update snippets 99 | - `GET /api/snippets/:id` - Retrieve a specific snippet 100 | 101 | ## Development 102 | 103 | Both backend and frontend run on port 3000: 104 | ```bash 105 | npm run dev:all 106 | ``` 107 | 108 | ## Contributing 109 | 110 | This is a demo application for educational purposes. If you find any bugs or want to suggest improvements, please open an issue or submit a pull request. 111 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | export const styles = { 2 | container: { 3 | maxWidth: '1200px', 4 | margin: '0 auto', 5 | padding: '20px', 6 | fontFamily: 'system-ui, -apple-system, sans-serif', 7 | }, 8 | header: { 9 | display: 'flex', 10 | justifyContent: 'space-between', 11 | alignItems: 'center', 12 | marginBottom: '20px', 13 | padding: '10px 0', 14 | borderBottom: '1px solid #eaeaea', 15 | }, 16 | headerControls: { 17 | display: "flex", 18 | gap: "10px", 19 | }, 20 | deleteButton: { 21 | backgroundColor: "#ff0000", 22 | color: "#fff", 23 | }, 24 | snippetListContainer: { 25 | display: "flex", 26 | flexDirection: "column" as "column", 27 | gap: "10px", 28 | }, 29 | editorContainer: { 30 | display: 'flex', 31 | flexDirection: 'column' as const, 32 | gap: '15px', 33 | }, 34 | controls: { 35 | display: 'flex', 36 | gap: '10px', 37 | flexWrap: 'wrap' as const, 38 | alignItems: 'center', 39 | }, 40 | input: { 41 | padding: '8px 12px', 42 | border: '1px solid #ddd', 43 | borderRadius: '4px', 44 | fontSize: '14px', 45 | flex: '1', 46 | minWidth: '200px', 47 | }, 48 | select: { 49 | padding: '8px 12px', 50 | border: '1px solid #ddd', 51 | borderRadius: '4px', 52 | fontSize: '14px', 53 | backgroundColor: 'white', 54 | }, 55 | button: { 56 | padding: '8px 16px', 57 | backgroundColor: '#0070f3', 58 | color: 'white', 59 | border: 'none', 60 | borderRadius: '4px', 61 | cursor: 'pointer', 62 | fontSize: '14px', 63 | transition: 'background-color 0.2s', 64 | '&:hover': { 65 | backgroundColor: '#0051cc', 66 | }, 67 | }, 68 | logoutButton: { 69 | backgroundColor: '#ff4444', 70 | '&:hover': { 71 | backgroundColor: '#cc0000', 72 | }, 73 | }, 74 | fileInput: { 75 | display: 'none', 76 | }, 77 | fileLabel: { 78 | padding: '8px 16px', 79 | backgroundColor: '#28a745', 80 | color: 'white', 81 | borderRadius: '4px', 82 | cursor: 'pointer', 83 | fontSize: '14px', 84 | transition: 'background-color 0.2s', 85 | '&:hover': { 86 | backgroundColor: '#218838', 87 | }, 88 | }, 89 | loginContainer: { 90 | maxWidth: '400px', 91 | margin: '100px auto', 92 | padding: '20px', 93 | borderRadius: '8px', 94 | boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', 95 | backgroundColor: 'white', 96 | }, 97 | loginForm: { 98 | display: 'flex', 99 | flexDirection: 'column' as const, 100 | gap: '15px', 101 | }, 102 | loginTitle: { 103 | textAlign: 'center' as const, 104 | marginBottom: '20px', 105 | color: '#333', 106 | }, 107 | editorWrapper: { 108 | border: '1px solid #ddd', 109 | borderRadius: '4px', 110 | overflow: 'hidden', 111 | }, 112 | }; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Elastic License 2.0 2 | 3 | URL: https://www.elastic.co/licensing/elastic-license 4 | 5 | ## Acceptance 6 | 7 | By using the software, you agree to all of the terms and conditions below. 8 | 9 | ## Copyright License 10 | 11 | The licensor grants you a non-exclusive, royalty-free, worldwide, 12 | non-sublicensable, non-transferable license to use, copy, distribute, make 13 | available, and prepare derivative works of the software, in each case subject to 14 | the limitations and conditions below. 15 | 16 | ## Limitations 17 | 18 | You may not provide the software to third parties as a hosted or managed 19 | service, where the service provides users with access to any substantial set of 20 | the features or functionality of the software. 21 | 22 | You may not move, change, disable, or circumvent the license key functionality 23 | in the software, and you may not remove or obscure any functionality in the 24 | software that is protected by the license key. 25 | 26 | You may not alter, remove, or obscure any licensing, copyright, or other notices 27 | of the licensor in the software. Any use of the licensor’s trademarks is subject 28 | to applicable law. 29 | 30 | ## Patents 31 | 32 | The licensor grants you a license, under any patent claims the licensor can 33 | license, or becomes able to license, to make, have made, use, sell, offer for 34 | sale, import and have imported the software, in each case subject to the 35 | limitations and conditions in this license. This license does not cover any 36 | patent claims that you cause to be infringed by modifications or additions to 37 | the software. If you or your company make any written claim that the software 38 | infringes or contributes to infringement of any patent, your patent license for 39 | the software granted under these terms ends immediately. If your company makes 40 | such a claim, your patent license ends immediately for work on behalf of your 41 | company. 42 | 43 | ## Notices 44 | 45 | You must ensure that anyone who gets a copy of any part of the software from you 46 | also gets a copy of these terms. 47 | 48 | If you modify the software, you must include in any modified copies of the 49 | software prominent notices stating that you have modified the software. 50 | 51 | ## No Other Rights 52 | 53 | These terms do not imply any licenses other than those expressly granted in 54 | these terms. 55 | 56 | ## Termination 57 | 58 | If you use the software in violation of these terms, such use is not licensed, 59 | and your licenses will automatically terminate. If the licensor provides you 60 | with a notice of your violation, and you cease all violation of this license no 61 | later than 30 days after you receive that notice, your licenses will be 62 | reinstated retroactively. However, if you violate these terms after such 63 | reinstatement, any additional violation of these terms will cause your licenses 64 | to terminate automatically and permanently. 65 | 66 | ## No Liability 67 | 68 | *As far as the law allows, the software comes as is, without any warranty or 69 | condition, and the licensor will not be liable to you for any damages arising 70 | out of these terms or the use or nature of the software, under any kind of 71 | legal claim.* 72 | 73 | ## Definitions 74 | 75 | The **licensor** is the entity offering these terms, and the **software** is the 76 | software the licensor makes available under these terms, including any portion 77 | of it. 78 | 79 | **you** refers to the individual or entity agreeing to these terms. 80 | 81 | **your company** is any legal entity, sole proprietorship, or other kind of 82 | organization that you work for, plus all organizations that have control over, 83 | are under the control of, or are under common control with that 84 | organization. **control** means ownership of substantially all the assets of an 85 | entity, or the power to direct its management and policies by vote, contract, or 86 | otherwise. Control can be direct or indirect. 87 | 88 | **your licenses** are all the licenses granted to you for the software under 89 | these terms. 90 | 91 | **use** means anything you do with the software requiring one of your licenses. 92 | 93 | **trademark** means trademarks, service marks, and similar rights. 94 | -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import CodeMirror from '@uiw/react-codemirror'; 4 | import { javascript } from '@codemirror/lang-javascript'; 5 | import { python } from '@codemirror/lang-python'; 6 | import { java } from '@codemirror/lang-java'; 7 | import { cpp } from '@codemirror/lang-cpp'; 8 | import { Snippet } from '../types'; 9 | import { styles } from '../styles'; 10 | import { API_ENDPOINTS } from '../config'; 11 | 12 | const languages = { 13 | typescript: javascript({ typescript: true }), 14 | javascript: javascript(), 15 | python: python(), 16 | java: java(), 17 | cpp: cpp() 18 | }; 19 | 20 | const storedUser = localStorage.getItem('user'); 21 | const userId = storedUser ? JSON.parse(storedUser).id : ''; 22 | 23 | function Editor() { 24 | const { id } = useParams(); 25 | const navigate = useNavigate(); 26 | const [snippet, setSnippet] = useState({ 27 | userId: '', 28 | title: '', 29 | content: '', 30 | language: 'typescript' 31 | }); 32 | const [saving, setSaving] = useState(false); 33 | 34 | useEffect(() => { 35 | if (id) { 36 | fetch(API_ENDPOINTS.SNIPPET(id)) 37 | .then(res => res.json()) 38 | .then(setSnippet) 39 | .catch(error => console.error("Error fetching snippet:", error)); 40 | } 41 | }, [id]); 42 | 43 | const handleSave = async () => { 44 | setSaving(true); 45 | try { 46 | // Retrieve the token from the stored user 47 | const storedUser = localStorage.getItem('user'); 48 | const token = storedUser ? JSON.parse(storedUser).token : ''; 49 | 50 | const response = await fetch(API_ENDPOINTS.SNIPPETS, { 51 | method: 'POST', 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | 'Authorization': `Bearer ${token}` // include token here 55 | }, 56 | body: JSON.stringify(snippet), 57 | }); 58 | if (!response.ok) { 59 | throw new Error('Failed to save snippet'); 60 | } 61 | const savedSnippet = await response.json(); 62 | navigate(`/snippet/${savedSnippet.id}`); 63 | } catch (error) { 64 | console.error('Error saving snippet:', error); 65 | } finally { 66 | setSaving(false); 67 | } 68 | }; 69 | 70 | // Delete handler with confirmation and state clearing 71 | const handleDelete = async () => { 72 | if (!snippet.id) return; 73 | const confirmDelete = window.confirm('Are you sure you want to delete this snippet?'); 74 | if (!confirmDelete) return; 75 | try { 76 | const response = await fetch(API_ENDPOINTS.SNIPPET(snippet.id), { 77 | method: 'DELETE', 78 | }); 79 | if (!response.ok) { 80 | throw new Error('Failed to delete snippet'); 81 | } 82 | // Clear snippet state so the Editor becomes empty 83 | setSnippet({ userId, title: '', content: '', language: 'typescript' }); 84 | // Navigate back to home (empty snippet page) 85 | navigate('/'); 86 | } catch (error) { 87 | console.error("Error deleting snippet:", error); 88 | } 89 | }; 90 | 91 | const handleFileUpload = (e: React.ChangeEvent) => { 92 | const file = e.target.files?.[0]; 93 | if (file) { 94 | const reader = new FileReader(); 95 | reader.onload = (e) => { 96 | setSnippet(prev => ({ 97 | ...prev, 98 | content: e.target?.result as string 99 | })); 100 | }; 101 | reader.readAsText(file); 102 | } 103 | }; 104 | 105 | return ( 106 |
107 |
108 | setSnippet(prev => ({ ...prev, title: e.target.value }))} 114 | /> 115 | 124 | 132 | 139 | {/* Delete button is shown only when a snippet exists (i.e. snippet.id is present) */} 140 | {snippet.id && ( 141 | 147 | )} 148 |
149 | 150 |
151 | setSnippet(prev => ({ ...prev, content: value }))} 157 | /> 158 |
159 |
160 | ); 161 | } 162 | 163 | export default Editor; 164 | --------------------------------------------------------------------------------