Баллы +1 за загрузку, убраны из лайков. Админ-панель (admin/admin123)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
48
server/src/routes/admin.ts
Normal file
48
server/src/routes/admin.ts
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user