├── .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 |
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 |
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 |
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 |
160 | );
161 | }
162 |
163 | export default Editor;
164 |
--------------------------------------------------------------------------------