Баллы +1 за загрузку, убраны из лайков. Админ-панель (admin/admin123)

This commit is contained in:
2026-05-29 12:13:42 +03:00
parent f8f220469c
commit 12701ffbc2
19 changed files with 250 additions and 62 deletions

View File

@@ -2,6 +2,7 @@ import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import bcrypt from 'bcryptjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = path.join(__dirname, '..', 'data.db');
@@ -26,10 +27,22 @@ export async function initDb(): Promise<SqlJsDatabase> {
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
points INTEGER DEFAULT 0,
is_admin INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Add is_admin column if missing (existing DB migration)
try { db.run('ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0'); } catch {}
// Auto-create admin if not exists
const admin = queryOne('SELECT id FROM users WHERE username = ?', ['admin']);
if (!admin) {
const hash = bcrypt.hashSync('admin123', 10);
execute('INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)', ['admin', hash]);
console.log('Admin user created: admin / admin123');
}
db.run(`
CREATE TABLE IF NOT EXISTS cats (
id INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@@ -7,6 +7,7 @@ import { initDb } from './db.js';
import authRoutes from './routes/auth.js';
import catRoutes from './routes/cats.js';
import likeRoutes from './routes/likes.js';
import adminRoutes from './routes/admin.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -24,6 +25,7 @@ async function main() {
app.use('/api/auth', authRoutes);
app.use('/api/cats', catRoutes);
app.use('/api/likes', likeRoutes);
app.use('/api/admin', adminRoutes);
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok' });

View File

@@ -1,15 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { queryOne } from '../db.js';
const JWT_SECRET = process.env.JWT_SECRET || 'catstagram-dev-secret-key-change-in-production';
export interface AuthRequest extends Request {
userId?: number;
username?: string;
isAdmin?: boolean;
}
export function generateToken(userId: number, username: string): string {
return jwt.sign({ userId, username }, JWT_SECRET, { expiresIn: '7d' });
export function generateToken(userId: number, username: string, isAdmin: boolean): string {
return jwt.sign({ userId, username, isAdmin }, JWT_SECRET, { expiresIn: '7d' });
}
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
@@ -21,11 +23,20 @@ export function authMiddleware(req: AuthRequest, res: Response, next: NextFuncti
const token = header.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; username: string };
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; username: string; isAdmin: boolean };
req.userId = decoded.userId;
req.username = decoded.username;
req.isAdmin = decoded.isAdmin;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
export function adminOnly(req: AuthRequest, res: Response, next: NextFunction): void {
if (!req.isAdmin) {
res.status(403).json({ error: 'Доступ только администратору' });
return;
}
next();
}

View File

@@ -0,0 +1,48 @@
import { Router, Response } from 'express';
import { queryAll, queryOne, execute } from '../db';
import { authMiddleware, adminOnly, AuthRequest } from '../middleware/auth.js';
const router = Router();
// All routes require admin
router.use(authMiddleware, adminOnly);
// Get all users
router.get('/users', (req: AuthRequest, res: Response) => {
const users = queryAll(
'SELECT id, username, points, is_admin, created_at FROM users ORDER BY points DESC'
);
res.json({ users });
});
// Delete any cat
router.delete('/cats/:id', (req: AuthRequest, res: Response) => {
const cat = queryOne('SELECT id FROM cats WHERE id = ?', [req.params.id]);
if (!cat) {
res.status(404).json({ error: 'Cat not found' });
return;
}
execute('DELETE FROM likes WHERE cat_id = ?', [req.params.id]);
execute('DELETE FROM cats WHERE id = ?', [req.params.id]);
res.json({ ok: true });
});
// Ban user (delete their cats and likes)
router.delete('/users/:id', (req: AuthRequest, res: Response) => {
const userId = parseInt(req.params.id);
const user = queryOne('SELECT id, username, is_admin FROM users WHERE id = ?', [userId]);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
if (user.is_admin) {
res.status(400).json({ error: 'Cannot delete admin' });
return;
}
execute('DELETE FROM likes WHERE cat_id IN (SELECT id FROM cats WHERE user_id = ?)', [userId]);
execute('DELETE FROM cats WHERE user_id = ?', [userId]);
execute('DELETE FROM users WHERE id = ?', [userId]);
res.json({ ok: true });
});
export default router;

View File

@@ -27,32 +27,32 @@ router.post('/register', (req: Request, res: Response) => {
const password_hash = bcrypt.hashSync(password, 10);
execute('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, password_hash]);
const user = queryOne('SELECT id, username, points FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, points, is_admin FROM users WHERE username = ?', [username]);
const token = generateToken(user.id, user.username);
res.status(201).json({ token, user });
const token = generateToken(user.id, user.username, !!user.is_admin);
res.status(201).json({ token, user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin } });
});
router.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
const user = queryOne('SELECT id, username, password_hash, points FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, password_hash, points, is_admin FROM users WHERE username = ?', [username]);
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
return;
}
const token = generateToken(user.id, user.username);
res.json({ token, user: { id: user.id, username: user.username, points: user.points } });
const token = generateToken(user.id, user.username, !!user.is_admin);
res.json({ token, user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin } });
});
router.get('/me', authMiddleware, (req: AuthRequest, res: Response) => {
const user = queryOne('SELECT id, username, points, created_at FROM users WHERE id = ?', [req.userId]);
const user = queryOne('SELECT id, username, points, is_admin, created_at FROM users WHERE id = ?', [req.userId]);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json({ user });
res.json({ user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin, created_at: user.created_at } });
});
export default router;
export default router;

View File

@@ -99,7 +99,7 @@ router.post('/', authMiddleware, upload.single('image'), (req: AuthRequest, res:
[req.userId, image_url, caption]
);
execute('UPDATE users SET points = points + 10 WHERE id = ?', [req.userId]);
execute('UPDATE users SET points = points + 1 WHERE id = ?', [req.userId]);
const cat = queryOne(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
@@ -120,7 +120,7 @@ router.delete('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
res.status(404).json({ error: 'Cat not found' });
return;
}
if (cat.user_id !== req.userId) {
if (cat.user_id !== req.userId && !req.isAdmin) {
res.status(403).json({ error: 'Not your cat' });
return;
}

View File

@@ -26,12 +26,10 @@ router.post('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
}
execute('INSERT INTO likes (cat_id, user_id) VALUES (?, ?)', [catId, userId]);
execute('UPDATE users SET points = points + 1 WHERE id = ?', [cat.user_id]);
const countResult = queryOne('SELECT COUNT(*) as count FROM likes WHERE cat_id = ?', [catId]);
const owner = queryOne('SELECT points FROM users WHERE id = ?', [cat.user_id]);
res.json({ liked: true, likesCount: countResult?.count || 0, ownerPoints: owner?.points || 0 });
res.json({ liked: true, likesCount: countResult?.count || 0 });
});
router.delete('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
@@ -51,12 +49,10 @@ router.delete('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
}
execute('DELETE FROM likes WHERE id = ?', [existing.id]);
execute('UPDATE users SET points = MAX(0, points - 1) WHERE id = ?', [cat.user_id]);
const countResult = queryOne('SELECT COUNT(*) as count FROM likes WHERE cat_id = ?', [catId]);
const owner = queryOne('SELECT points FROM users WHERE id = ?', [cat.user_id]);
res.json({ liked: false, likesCount: countResult?.count || 0, ownerPoints: owner?.points || 0 });
res.json({ liked: false, likesCount: countResult?.count || 0 });
});
router.get('/:catId/status', authMiddleware, (req: AuthRequest, res: Response) => {
@@ -67,4 +63,4 @@ router.get('/:catId/status', authMiddleware, (req: AuthRequest, res: Response) =
res.json({ liked: !!liked });
});
export default router;
export default router;