Zion Boggan zionboggan.com ↗
118 lines · javascript
History for this file →
1
const express = require('express');
2
const Database = require('better-sqlite3');
3
const Joi = require('joi');
4
const cors = require('cors');
5
const helmet = require('helmet');
6
const morgan = require('morgan');
7
const swaggerUi = require('swagger-ui-express');
8
const jwt = require('jsonwebtoken');
9
const bcrypt = require('bcryptjs');
10
const rateLimit = require('express-rate-limit');
11
 
12
const app = express();
13
const PORT = process.env.PORT || 3000;
14
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-this';
15
 
16
app.use(helmet());
17
app.use(cors());
18
app.use(express.json());
19
app.use(morgan('combined'));
20
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
21
app.use('/api/', limiter);
22
 
23
const db = new Database(':memory:');
24
db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)`);
25
db.exec(`CREATE TABLE videos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`);
26
const hashed = bcrypt.hashSync('admin', 10);
27
db.prepare(`INSERT INTO users (username, password) VALUES ('admin', ?)`).run(hashed);
28
 
29
const authenticateToken = (req, res, next) => {
30
  const token = req.header('Authorization')?.replace('Bearer ', '');
31
  if (!token) return res.status(401).json({ message: 'Access token required' });
32
  jwt.verify(token, JWT_SECRET, (err, user) => {
33
    if (err) return res.status(403).json({ message: 'Invalid token' });
34
    req.user = user;
35
    next();
36
  });
37
};
38
 
39
const videoSchema = Joi.object({
40
  title: Joi.string().min(1).max(255).required(),
41
  description: Joi.string().min(1).max(1000).required(),
42
  url: Joi.string().uri().required()
43
});
44
 
45
app.post('/api/login', (req, res) => {
46
  const { username, password } = req.body;
47
  const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
48
  if (!user || !bcrypt.compareSync(password, user.password)) {
49
    return res.status(401).json({ message: 'Invalid credentials' });
50
  }
51
  const token = jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '24h' });
52
  res.json({ token });
53
});
54
 
55
app.get('/api/videos', authenticateToken, (req, res) => {
56
  const page = parseInt(req.query.page) || 1;
57
  const limit = parseInt(req.query.limit) || 10;
58
  const offset = (page - 1) * limit;
59
  const rows = db.prepare('SELECT * FROM videos ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
60
  const count = db.prepare('SELECT COUNT(*) as count FROM videos').get();
61
  res.json({ data: rows, pagination: { page, limit, total: count.count, pages: Math.ceil(count.count / limit) } });
62
});
63
 
64
app.get('/api/videos/:id', authenticateToken, (req, res) => {
65
  const row = db.prepare('SELECT * FROM videos WHERE id = ?').get(req.params.id);
66
  if (!row) return res.status(404).json({ message: 'Video not found' });
67
  res.json(row);
68
});
69
 
70
app.post('/api/videos', authenticateToken, (req, res) => {
71
  const { error } = videoSchema.validate(req.body);
72
  if (error) return res.status(400).json({ message: error.details[0].message });
73
  const result = db.prepare('INSERT INTO videos (title, description, url) VALUES (?, ?, ?)').run(req.body.title, req.body.description, req.body.url);
74
  res.status(201).json({ id: result.lastInsertRowid });
75
});
76
 
77
app.put('/api/videos/:id', authenticateToken, (req, res) => {
78
  const { error } = videoSchema.validate(req.body);
79
  if (error) return res.status(400).json({ message: error.details[0].message });
80
  const result = db.prepare('UPDATE videos SET title = ?, description = ?, url = ? WHERE id = ?').run(req.body.title, req.body.description, req.body.url, req.params.id);
81
  if (result.changes === 0) return res.status(404).json({ message: 'Video not found' });
82
  res.json({ updated: true });
83
});
84
 
85
app.delete('/api/videos/:id', authenticateToken, (req, res) => {
86
  const result = db.prepare('DELETE FROM videos WHERE id = ?').run(req.params.id);
87
  if (result.changes === 0) return res.status(404).json({ message: 'Video not found' });
88
  res.status(204).send();
89
});
90
 
91
app.get('/api/search', authenticateToken, (req, res) => {
92
  const q = req.query.q;
93
  if (!q) return res.status(400).json({ message: 'Query required' });
94
  const rows = db.prepare('SELECT * FROM videos WHERE title LIKE ? OR description LIKE ?').all(`%${q}%`, `%${q}%`);
95
  res.json(rows);
96
});
97
 
98
app.use('/docs', swaggerUi.serve, swaggerUi.setup({
99
  openapi: '3.0.0',
100
  info: { title: 'Video Library API', version: '1.0.0' },
101
  paths: {
102
    '/api/login': {
103
      post: {
104
        summary: 'Login',
105
        requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { username: { type: 'string' }, password: { type: 'string' } } } } } },
106
        responses: { '200': { description: 'Token' } }
107
      }
108
    },
109
    '/api/videos': {
110
      get: { summary: 'List videos', responses: { '200': { description: 'Videos' } } },
111
      post: { summary: 'Create video', responses: { '201': { description: 'Created' } } }
112
    }
113
  }
114
}));
115
 
116
app.use((err, req, res, next) => res.status(500).json({ message: err.message }));
117
 
118
app.listen(PORT, () => console.log(`Video API running on http://localhost:${PORT}`));