Fix likes display, add avatar system (presets+upload), improve desktop layout, fix profile viewing

This commit is contained in:
2026-05-29 14:21:40 +03:00
parent 3664b60f10
commit 025e52e6b1
19 changed files with 346 additions and 94 deletions

View File

@@ -34,6 +34,7 @@ export async function initDb(): Promise<SqlJsDatabase> {
// Add is_admin column if missing (existing DB migration)
try { db.run('ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0'); } catch {}
try { db.run('ALTER TABLE users ADD COLUMN avatar_url TEXT DEFAULT NULL'); } catch {}
// Auto-create admin if not exists
const admin = queryOne('SELECT id FROM users WHERE username = ?', ['admin']);

View File

@@ -9,6 +9,7 @@ import catRoutes from './routes/cats.js';
import likeRoutes from './routes/likes.js';
import adminRoutes from './routes/admin.js';
import commentRoutes from './routes/comments.js';
import userRoutes from './routes/users.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -27,6 +28,7 @@ async function main() {
app.use('/api/cats', catRoutes);
app.use('/api/likes', likeRoutes);
app.use('/api/comments', commentRoutes);
app.use('/api/users', userRoutes);
app.use('/api/admin', adminRoutes);
app.get('/api/health', (_req, res) => {

View File

@@ -10,7 +10,7 @@ router.use(authMiddleware, adminOnly);
router.get('/users', (_req: AuthRequest, res: Response) => {
const users = queryAll(
'SELECT id, username, points, is_admin, created_at FROM users ORDER BY points DESC'
'SELECT id, username, points, is_admin, avatar_url, created_at FROM users ORDER BY points DESC'
);
res.json({ users });
});

View File

@@ -1,8 +1,43 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
import { queryOne, execute } from '../db';
import { generateToken, authMiddleware, AuthRequest } from '../middleware/auth.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const uploadsDir = path.join(__dirname, '..', '..', 'uploads');
const avatarStorage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, uploadsDir),
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `avatar-${Date.now()}-${Math.random().toString(36).slice(2, 6)}${ext}`);
},
});
const avatarUpload = multer({
storage: avatarStorage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
const allowed = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
const ext = path.extname(file.originalname).toLowerCase();
cb(null, allowed.includes(ext));
},
});
function userToResponse(u: any) {
return {
id: u.id,
username: u.username,
points: u.points,
is_admin: !!u.is_admin,
avatar_url: u.avatar_url || null,
created_at: u.created_at || undefined,
};
}
const router = Router();
router.post('/register', (req: Request, res: Response) => {
@@ -27,32 +62,58 @@ 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, is_admin FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, points, is_admin, avatar_url FROM users WHERE username = ?', [username]);
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 } });
res.status(201).json({ token, user: userToResponse(user) });
});
router.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
const user = queryOne('SELECT id, username, password_hash, points, is_admin FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, password_hash, points, is_admin, avatar_url 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, !!user.is_admin);
res.json({ token, user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin } });
res.json({ token, user: userToResponse(user) });
});
router.get('/me', authMiddleware, (req: AuthRequest, res: Response) => {
const user = queryOne('SELECT id, username, points, is_admin, created_at FROM users WHERE id = ?', [req.userId]);
const user = queryOne('SELECT id, username, points, is_admin, avatar_url, created_at FROM users WHERE id = ?', [req.userId]);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json({ user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin, created_at: user.created_at } });
res.json({ user: userToResponse(user) });
});
router.put('/avatar', authMiddleware, avatarUpload.single('avatar'), (req: AuthRequest, res: Response) => {
if (!req.file) {
res.status(400).json({ error: 'Нужно выбрать изображение' });
return;
}
const avatar_url = `/uploads/${req.file.filename}`;
execute('UPDATE users SET avatar_url = ? WHERE id = ?', [avatar_url, req.userId]);
const user = queryOne('SELECT id, username, points, is_admin, avatar_url, created_at FROM users WHERE id = ?', [req.userId]);
res.json({ user: userToResponse(user) });
});
router.put('/avatar/preset', authMiddleware, (req: AuthRequest, res: Response) => {
const { preset } = req.body;
if (!preset) {
res.status(400).json({ error: 'Укажите пресет аватара' });
return;
}
const avatar_url = `preset:${preset}`;
execute('UPDATE users SET avatar_url = ? WHERE id = ?', [avatar_url, req.userId]);
const user = queryOne('SELECT id, username, points, is_admin, avatar_url, created_at FROM users WHERE id = ?', [req.userId]);
res.json({ user: userToResponse(user) });
});
export default router;

View File

@@ -35,7 +35,7 @@ router.get('/', (req: AuthRequest, res: Response) => {
const cats = queryAll(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
u.username, u.points AS user_points,
u.username, u.points AS user_points, u.avatar_url AS user_avatar_url,
(SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count,
(SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count
FROM cats c
@@ -55,7 +55,7 @@ router.get('/user/:userId', (req: AuthRequest, res: Response) => {
const userId = parseInt(req.params.userId);
const cats = queryAll(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
u.username, u.points AS user_points,
u.username, u.points AS user_points, u.avatar_url AS user_avatar_url,
(SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count,
(SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count
FROM cats c
@@ -71,7 +71,7 @@ router.get('/user/:userId', (req: AuthRequest, res: Response) => {
router.get('/:id', (req: AuthRequest, res: Response) => {
const cat = queryOne(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
u.username, u.points AS user_points,
u.username, u.points AS user_points, u.avatar_url AS user_avatar_url,
(SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count,
(SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count
FROM cats c
@@ -106,7 +106,7 @@ router.post('/', authMiddleware, upload.single('image'), (req: AuthRequest, res:
const cat = queryOne(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
u.username, u.points AS user_points,
u.username, u.points AS user_points, u.avatar_url AS user_avatar_url,
0 AS likes_count,
0 AS comments_count
FROM cats c

View File

@@ -1,5 +1,5 @@
import { Router, Response } from 'express';
import { queryOne, execute } from '../db';
import { queryOne, queryAll, execute } from '../db';
import { authMiddleware, AuthRequest } from '../middleware/auth.js';
const router = Router();
@@ -63,4 +63,21 @@ router.get('/:catId/status', authMiddleware, (req: AuthRequest, res: Response) =
res.json({ liked: !!liked });
});
router.post('/batch/status', authMiddleware, (req: AuthRequest, res: Response) => {
const userId = req.userId!;
const { catIds } = req.body as { catIds: number[] };
if (!Array.isArray(catIds) || catIds.length === 0) {
res.json({ likedIds: [] });
return;
}
const likes = queryAll(
`SELECT cat_id FROM likes WHERE user_id = ? AND cat_id IN (${catIds.map(() => '?').join(',')})`,
[userId, ...catIds]
);
res.json({ likedIds: likes.map((l: any) => l.cat_id) });
});
export default router;

View File

@@ -0,0 +1,26 @@
import { Router, Response } from 'express';
import { queryOne, queryAll } from '../db';
import { AuthRequest } from '../middleware/auth.js';
const router = Router();
router.get('/:id', (req: AuthRequest, res: Response) => {
const userId = parseInt(req.params.id);
const user = queryOne('SELECT id, username, points, is_admin, avatar_url, created_at FROM users WHERE id = ?', [userId]);
if (!user) {
res.status(404).json({ error: 'Пользователь не найден' });
return;
}
res.json({
user: {
id: user.id,
username: user.username,
points: user.points,
is_admin: !!user.is_admin,
avatar_url: user.avatar_url || null,
created_at: user.created_at,
},
});
});
export default router;