Fix likes display, add avatar system (presets+upload), improve desktop layout, fix profile viewing
This commit is contained in:
@@ -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']);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
26
server/src/routes/users.ts
Normal file
26
server/src/routes/users.ts
Normal 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;
|
||||
Reference in New Issue
Block a user