Initial: Fastify API + DB schema + collab setup

- Fastify API on :3000 (tasks, groups, auth, connectors)
- PostgreSQL schema: users, tasks, task_groups, goals, connectors
- 8 default task groups
- JWT auth (register/login/me)
- API Bridge framework with webhooks
- CLAUDE.md + polling scripts
- Collab worker integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude CLI Agent
2026-03-29 09:57:56 +00:00
commit 9d075455e3
13 changed files with 2430 additions and 0 deletions

29
api/src/routes/auth.js Normal file
View File

@@ -0,0 +1,29 @@
// Task Team — Auth Routes — 2026-03-29
async function authRoutes(app) {
// Simple JWT auth for now, Supabase integration later
app.post('/auth/register', async (req) => {
const { email, name, phone, password } = req.body;
const { rows } = await app.db.query(
'INSERT INTO users (email, name, phone) VALUES ($1, $2, $3) RETURNING id, email, name',
[email, name, phone]
);
const token = app.jwt.sign({ id: rows[0].id, email: rows[0].email });
return { data: { user: rows[0], token } };
});
app.post('/auth/login', async (req) => {
const { email } = req.body;
const { rows } = await app.db.query('SELECT id, email, name FROM users WHERE email = $1', [email]);
if (!rows.length) throw { statusCode: 401, message: 'User not found' };
const token = app.jwt.sign({ id: rows[0].id, email: rows[0].email });
return { data: { user: rows[0], token } };
});
app.get('/auth/me', { preHandler: [async (req) => { await req.jwtVerify() }] }, async (req) => {
const { rows } = await app.db.query('SELECT id, email, name, phone, language, settings FROM users WHERE id = $1', [req.user.id]);
if (!rows.length) throw { statusCode: 404, message: 'User not found' };
return { data: rows[0] };
});
}
module.exports = authRoutes;

View File

@@ -0,0 +1,55 @@
// Task Team — API Bridge / Connectors — 2026-03-29
async function connectorRoutes(app) {
// List connectors
app.get('/connectors', async (req) => {
const { rows } = await app.db.query('SELECT id, type, enabled, last_sync_at, created_at FROM connectors ORDER BY created_at DESC');
return { data: rows };
});
// Get connector
app.get('/connectors/:id', async (req) => {
const { rows } = await app.db.query('SELECT * FROM connectors WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
return { data: rows[0] };
});
// Create connector
app.post('/connectors', async (req) => {
const { type, config, user_id } = req.body;
const { rows } = await app.db.query(
'INSERT INTO connectors (user_id, type, config) VALUES ($1, $2, $3) RETURNING *',
[user_id, type, JSON.stringify(config || {})]
);
return { data: rows[0] };
});
// Sync connector
app.post('/connectors/:id/sync', async (req) => {
const { rows } = await app.db.query('SELECT * FROM connectors WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
// TODO: dispatch sync job to n8n or Redis queue
await app.db.query('UPDATE connectors SET last_sync_at = NOW() WHERE id = $1', [req.params.id]);
return { status: 'sync_started', connector: rows[0].type };
});
// Generic webhook
app.post('/connectors/webhook/:type', async (req) => {
const { type } = req.params;
// Log webhook
app.log.info({ type, body: req.body }, 'Webhook received');
// TODO: process based on type (odoo, moodle, pohoda, generic)
return { status: 'received', type };
});
// Toggle connector
app.put('/connectors/:id/toggle', async (req) => {
const { rows } = await app.db.query(
'UPDATE connectors SET enabled = NOT enabled, updated_at = NOW() WHERE id = $1 RETURNING id, type, enabled',
[req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
return { data: rows[0] };
});
}
module.exports = connectorRoutes;

62
api/src/routes/groups.js Normal file
View File

@@ -0,0 +1,62 @@
// Task Team — Groups CRUD — 2026-03-29
async function groupRoutes(app) {
app.get('/groups', async (req) => {
const { rows } = await app.db.query(
'SELECT * FROM task_groups ORDER BY order_index ASC'
);
return { data: rows };
});
app.get('/groups/:id', async (req) => {
const { rows } = await app.db.query('SELECT * FROM task_groups WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
return { data: rows[0] };
});
app.post('/groups', async (req) => {
const { name, color, icon, order_index, time_zones, user_id } = req.body;
const { rows } = await app.db.query(
'INSERT INTO task_groups (user_id, name, color, icon, order_index, time_zones) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *',
[user_id, name, color, icon || '', order_index || 0, JSON.stringify(time_zones || [])]
);
return { data: rows[0] };
});
app.put('/groups/:id', async (req) => {
const { name, color, icon, order_index, time_zones } = req.body;
const { rows } = await app.db.query(
`UPDATE task_groups SET name=COALESCE($1,name), color=COALESCE($2,color), icon=COALESCE($3,icon),
order_index=COALESCE($4,order_index), time_zones=COALESCE($5,time_zones), updated_at=NOW()
WHERE id=$6 RETURNING *`,
[name, color, icon, order_index, time_zones ? JSON.stringify(time_zones) : null, req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
return { data: rows[0] };
});
app.delete('/groups/:id', async (req) => {
const { rowCount } = await app.db.query('DELETE FROM task_groups WHERE id = $1', [req.params.id]);
if (!rowCount) throw { statusCode: 404, message: 'Group not found' };
return { status: 'deleted' };
});
app.put('/groups/reorder', async (req) => {
const { order } = req.body; // [{id, order_index}]
const client = await app.db.connect();
try {
await client.query('BEGIN');
for (const item of order) {
await client.query('UPDATE task_groups SET order_index=$1, updated_at=NOW() WHERE id=$2', [item.order_index, item.id]);
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
return { status: 'ok' };
});
}
module.exports = groupRoutes;

88
api/src/routes/tasks.js Normal file
View File

@@ -0,0 +1,88 @@
// Task Team — Tasks CRUD — 2026-03-29
async function taskRoutes(app) {
// List tasks
app.get('/tasks', async (req, reply) => {
const { status, group_id, limit = 50, offset = 0 } = req.query;
let query = 'SELECT t.*, tg.name as group_name, tg.color as group_color, tg.icon as group_icon FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id WHERE 1=1';
const params = [];
if (status) { params.push(status); query += ` AND t.status = $${params.length}`; }
if (group_id) { params.push(group_id); query += ` AND t.group_id = $${params.length}`; }
query += ' ORDER BY t.scheduled_at ASC NULLS LAST, t.priority DESC, t.created_at DESC';
params.push(limit); query += ` LIMIT $${params.length}`;
params.push(offset); query += ` OFFSET $${params.length}`;
const { rows } = await app.db.query(query, params);
return { data: rows, total: rows.length };
});
// Get single task
app.get('/tasks/:id', async (req) => {
const { rows } = await app.db.query(
'SELECT t.*, tg.name as group_name, tg.color as group_color FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id WHERE t.id = $1',
[req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Task not found' };
return { data: rows[0] };
});
// Create task
app.post('/tasks', async (req) => {
const { title, description, status, group_id, priority, scheduled_at, due_at, assigned_to } = req.body;
const { rows } = await app.db.query(
`INSERT INTO tasks (title, description, status, group_id, priority, scheduled_at, due_at, assigned_to)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
[title, description || '', status || 'pending', group_id, priority || 'medium', scheduled_at, due_at, assigned_to || []]
);
return { data: rows[0] };
});
// Update task
app.put('/tasks/:id', async (req) => {
const fields = req.body;
const sets = [];
const params = [];
let i = 1;
for (const [key, value] of Object.entries(fields)) {
if (['title','description','status','group_id','priority','scheduled_at','due_at','assigned_to','completed_at'].includes(key)) {
sets.push(`${key} = $${i}`);
params.push(value);
i++;
}
}
if (fields.status === 'completed' && !fields.completed_at) {
sets.push(`completed_at = NOW()`);
}
sets.push(`updated_at = NOW()`);
params.push(req.params.id);
const { rows } = await app.db.query(
`UPDATE tasks SET ${sets.join(', ')} WHERE id = $${i} RETURNING *`, params
);
if (!rows.length) throw { statusCode: 404, message: 'Task not found' };
return { data: rows[0] };
});
// Delete task
app.delete('/tasks/:id', async (req) => {
const { rowCount } = await app.db.query('DELETE FROM tasks WHERE id = $1', [req.params.id]);
if (!rowCount) throw { statusCode: 404, message: 'Task not found' };
return { status: 'deleted' };
});
// Task comments
app.get('/tasks/:id/comments', async (req) => {
const { rows } = await app.db.query(
'SELECT * FROM task_comments WHERE task_id = $1 ORDER BY created_at ASC', [req.params.id]
);
return { data: rows };
});
app.post('/tasks/:id/comments', async (req) => {
const { content, is_ai } = req.body;
const { rows } = await app.db.query(
'INSERT INTO task_comments (task_id, content, is_ai) VALUES ($1, $2, $3) RETURNING *',
[req.params.id, content, is_ai || false]
);
return { data: rows[0] };
});
}
module.exports = taskRoutes;