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:
41
api/src/index.js
Normal file
41
api/src/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Task Team — API Server — 2026-03-29
|
||||
require('dotenv').config();
|
||||
const Fastify = require('fastify');
|
||||
const cors = require('@fastify/cors');
|
||||
const jwt = require('@fastify/jwt');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
// Database pool
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://taskteam:TaskTeam2026!@10.10.10.10:5432/taskteam'
|
||||
});
|
||||
|
||||
// Plugins
|
||||
app.register(cors, { origin: true });
|
||||
app.register(jwt, { secret: process.env.JWT_SECRET || 'taskteam-jwt-secret-2026' });
|
||||
|
||||
// Decorate with db
|
||||
app.decorate('db', pool);
|
||||
|
||||
// Health check
|
||||
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||
|
||||
// Register routes
|
||||
app.register(require('./routes/tasks'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/groups'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/auth'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/connectors'), { prefix: '/api/v1' });
|
||||
|
||||
// Start
|
||||
const start = async () => {
|
||||
try {
|
||||
await app.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
|
||||
console.log('Task Team API listening on port ' + (process.env.PORT || 3000));
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
start();
|
||||
29
api/src/routes/auth.js
Normal file
29
api/src/routes/auth.js
Normal 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;
|
||||
55
api/src/routes/connectors.js
Normal file
55
api/src/routes/connectors.js
Normal 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
62
api/src/routes/groups.js
Normal 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
88
api/src/routes/tasks.js
Normal 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;
|
||||
Reference in New Issue
Block a user