feat: MySQL integration, Docker setup, drag-and-drop kanban

This commit is contained in:
tusuii
2026-02-16 10:20:27 +05:30
parent 5d8af1f173
commit 892a2ceba1
24 changed files with 919 additions and 196 deletions

79
server/routes/auth.js Normal file
View File

@@ -0,0 +1,79 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import pool from '../db.js';
import { randomUUID } from 'crypto';
const router = Router();
// POST /api/auth/login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
if (rows.length === 0) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const user = rows[0];
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid email or password' });
}
res.json({
id: user.id, name: user.name, role: user.role, email: user.email,
color: user.color, avatar: user.avatar, dept: user.dept,
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/auth/register
router.post('/register', async (req, res) => {
try {
const { name, email, password, role, dept } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ error: 'Name, email and password required' });
}
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
if (existing.length > 0) {
return res.status(409).json({ error: 'Email already registered' });
}
const id = randomUUID();
const password_hash = await bcrypt.hash(password, 10);
const avatar = name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
const colors = ['#818cf8', '#f59e0b', '#34d399', '#f472b6', '#fb923c', '#60a5fa', '#a78bfa'];
const color = colors[Math.floor(Math.random() * colors.length)];
await pool.query(
'INSERT INTO users (id, name, role, email, password_hash, color, avatar, dept) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, name, role || 'employee', email, password_hash, color, avatar, dept || '']
);
res.status(201).json({ id, name, role: role || 'employee', email, color, avatar, dept: dept || '' });
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// GET /api/auth/users
router.get('/users', async (_req, res) => {
try {
const [rows] = await pool.query('SELECT id, name, role, email, color, avatar, dept FROM users');
res.json(rows);
} catch (err) {
console.error('Get users error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

202
server/routes/tasks.js Normal file
View File

@@ -0,0 +1,202 @@
import { Router } from 'express';
import pool from '../db.js';
import { randomUUID } from 'crypto';
const router = Router();
// Helper: fetch full task with subtasks, comments, activities, tags
async function getFullTask(taskId) {
const [taskRows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [taskId]);
if (taskRows.length === 0) return null;
const task = taskRows[0];
const [subtasks] = await pool.query('SELECT id, title, done FROM subtasks WHERE task_id = ? ORDER BY id', [taskId]);
const [comments] = await pool.query('SELECT id, user_id AS userId, text, timestamp FROM comments WHERE task_id = ? ORDER BY timestamp', [taskId]);
const [activities] = await pool.query('SELECT id, text, timestamp FROM activities WHERE task_id = ? ORDER BY timestamp', [taskId]);
const [tagRows] = await pool.query('SELECT tag FROM task_tags WHERE task_id = ?', [taskId]);
return {
id: task.id,
title: task.title,
description: task.description || '',
status: task.status,
priority: task.priority,
assignee: task.assignee_id || '',
reporter: task.reporter_id || '',
dueDate: task.due_date ? task.due_date.toISOString().split('T')[0] : '',
tags: tagRows.map(r => r.tag),
subtasks: subtasks.map(s => ({ id: s.id, title: s.title, done: !!s.done })),
comments: comments.map(c => ({ id: c.id, userId: c.userId, text: c.text, timestamp: c.timestamp?.toISOString() || '' })),
activity: activities.map(a => ({ id: a.id, text: a.text, timestamp: a.timestamp?.toISOString() || '' })),
};
}
// GET /api/tasks
router.get('/', async (_req, res) => {
try {
const [taskRows] = await pool.query('SELECT id FROM tasks ORDER BY created_at DESC');
const tasks = await Promise.all(taskRows.map(t => getFullTask(t.id)));
res.json(tasks.filter(Boolean));
} catch (err) {
console.error('Get tasks error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks
router.post('/', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { title, description, status, priority, assignee, reporter, dueDate, tags, subtasks } = req.body;
const id = randomUUID();
await conn.query(
'INSERT INTO tasks (id, title, description, status, priority, assignee_id, reporter_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, title, description || '', status || 'todo', priority || 'medium', assignee || null, reporter || null, dueDate || null]
);
// Insert tags
if (tags && tags.length > 0) {
const tagValues = tags.map(tag => [id, tag]);
await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]);
}
// Insert subtasks
if (subtasks && subtasks.length > 0) {
for (const st of subtasks) {
await conn.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)',
[st.id || randomUUID(), id, st.title, st.done || false]);
}
}
// Add creation activity
const actId = randomUUID();
await conn.query('INSERT INTO activities (id, task_id, text) VALUES (?, ?, ?)',
[actId, id, '📝 Task created']);
await conn.commit();
const task = await getFullTask(id);
res.status(201).json(task);
} catch (err) {
await conn.rollback();
console.error('Create task error:', err);
res.status(500).json({ error: 'Internal server error' });
} finally {
conn.release();
}
});
// PUT /api/tasks/:id
router.put('/:id', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { title, description, status, priority, assignee, reporter, dueDate, tags } = req.body;
const taskId = req.params.id;
// Check task exists
const [existing] = await conn.query('SELECT id FROM tasks WHERE id = ?', [taskId]);
if (existing.length === 0) {
await conn.rollback();
return res.status(404).json({ error: 'Task not found' });
}
await conn.query(
`UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description),
status = COALESCE(?, status), priority = COALESCE(?, priority),
assignee_id = COALESCE(?, assignee_id), reporter_id = COALESCE(?, reporter_id),
due_date = COALESCE(?, due_date) WHERE id = ?`,
[title, description, status, priority, assignee, reporter, dueDate, taskId]
);
// Update tags if provided
if (tags !== undefined) {
await conn.query('DELETE FROM task_tags WHERE task_id = ?', [taskId]);
if (tags.length > 0) {
const tagValues = tags.map(tag => [taskId, tag]);
await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]);
}
}
await conn.commit();
const task = await getFullTask(taskId);
res.json(task);
} catch (err) {
await conn.rollback();
console.error('Update task error:', err);
res.status(500).json({ error: 'Internal server error' });
} finally {
conn.release();
}
});
// POST /api/tasks/:id/subtasks
router.post('/:id/subtasks', async (req, res) => {
try {
const { title } = req.body;
const id = randomUUID();
await pool.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)',
[id, req.params.id, title, false]);
res.status(201).json({ id, title, done: false });
} catch (err) {
console.error('Add subtask error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// PUT /api/tasks/:id/subtasks/:sid
router.put('/:id/subtasks/:sid', async (req, res) => {
try {
const { done } = req.body;
await pool.query('UPDATE subtasks SET done = ? WHERE id = ? AND task_id = ?',
[done, req.params.sid, req.params.id]);
res.json({ success: true });
} catch (err) {
console.error('Toggle subtask error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks/:id/comments
router.post('/:id/comments', async (req, res) => {
try {
const { userId, text } = req.body;
const id = randomUUID();
const timestamp = new Date();
await pool.query('INSERT INTO comments (id, task_id, user_id, text, timestamp) VALUES (?, ?, ?, ?, ?)',
[id, req.params.id, userId, text, timestamp]);
res.status(201).json({ id, userId, text, timestamp: timestamp.toISOString() });
} catch (err) {
console.error('Add comment error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks/:id/activity
router.post('/:id/activity', async (req, res) => {
try {
const { text } = req.body;
const id = randomUUID();
const timestamp = new Date();
await pool.query('INSERT INTO activities (id, task_id, text, timestamp) VALUES (?, ?, ?, ?)',
[id, req.params.id, text, timestamp]);
res.status(201).json({ id, text, timestamp: timestamp.toISOString() });
} catch (err) {
console.error('Add activity error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE /api/tasks/:id
router.delete('/:id', async (req, res) => {
try {
await pool.query('DELETE FROM tasks WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (err) {
console.error('Delete task error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;