Initial Planlink implementation
This commit is contained in:
Generated
+1420
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "planlink-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "node src/index.js",
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.11.0",
|
||||
"openai": "^4.70.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const morgan = require("morgan");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const routes = require("./routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.CLIENT_ORIGIN || "http://localhost:5173",
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
app.use(cookieParser());
|
||||
app.use(morgan("dev"));
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({ ok: true, service: "planlink" });
|
||||
});
|
||||
|
||||
app.use("/api", routes);
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,12 @@
|
||||
const dotenv = require("dotenv");
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const required = ["JWT_SECRET"]; // Extend when wiring auth
|
||||
|
||||
for (const key of required) {
|
||||
if (!process.env[key]) {
|
||||
// Keep a warning for now; enforce later when auth is wired
|
||||
console.warn(`[env] Missing ${key}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
let pool;
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT || 3306),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
module.exports = { getPool };
|
||||
@@ -0,0 +1,14 @@
|
||||
const { getPool } = require("./pool");
|
||||
|
||||
async function query(sql, params = []) {
|
||||
const pool = getPool();
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getOne(sql, params = []) {
|
||||
const rows = await query(sql, params);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
module.exports = { query, getOne };
|
||||
@@ -0,0 +1,13 @@
|
||||
require("./config/env");
|
||||
const http = require("http");
|
||||
const app = require("./app");
|
||||
const { initSocket } = require("./realtime/socket");
|
||||
|
||||
const port = Number(process.env.PORT || 4000);
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
initSocket(httpServer);
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`planlink server listening on :${port}`);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.planlink_token;
|
||||
if (!token) return res.status(401).json({ ok: false, message: "unauthorized" });
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = payload;
|
||||
return next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ ok: false, message: "invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { requireAuth };
|
||||
@@ -0,0 +1,21 @@
|
||||
const { query } = require("../db/queries");
|
||||
|
||||
let ioRef;
|
||||
|
||||
function setIO(io) {
|
||||
ioRef = io;
|
||||
}
|
||||
|
||||
function emitBoard(boardId, event, payload) {
|
||||
if (!ioRef || !boardId) return;
|
||||
ioRef.to(`board:${boardId}`).emit(event, payload);
|
||||
}
|
||||
|
||||
async function logActivity({ projectId, boardId, cardId, actorId, type, payload }) {
|
||||
await query(
|
||||
"INSERT INTO activities (project_id, board_id, card_id, actor_id, type, payload) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[projectId, boardId || null, cardId || null, actorId, type, payload ? JSON.stringify(payload) : null]
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { setIO, emitBoard, logActivity };
|
||||
@@ -0,0 +1,27 @@
|
||||
const { Server } = require("socket.io");
|
||||
const { setIO } = require("./events");
|
||||
|
||||
function initSocket(httpServer) {
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.CLIENT_ORIGIN || "http://localhost:5173",
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
setIO(io);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("board:join", (boardId) => {
|
||||
if (boardId) socket.join(`board:${boardId}`);
|
||||
});
|
||||
|
||||
socket.on("board:leave", (boardId) => {
|
||||
if (boardId) socket.leave(`board:${boardId}`);
|
||||
});
|
||||
});
|
||||
|
||||
return io;
|
||||
}
|
||||
|
||||
module.exports = { initSocket };
|
||||
@@ -0,0 +1,82 @@
|
||||
const express = require("express");
|
||||
const { getOne, query } = require("../db/queries");
|
||||
const { hashPassword, verifyPassword } = require("../utils/password");
|
||||
const { signUser } = require("../utils/jwt");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const COOKIE_OPTS = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: false
|
||||
};
|
||||
|
||||
router.post("/register", async (req, res) => {
|
||||
const { email, password, name } = req.body || {};
|
||||
if (!email || !password || !name) {
|
||||
return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
}
|
||||
|
||||
const existing = await getOne("SELECT id FROM users WHERE email = ?", [email]);
|
||||
if (existing) {
|
||||
return res.status(409).json({ ok: false, message: "email exists" });
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const result = await query(
|
||||
"INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)",
|
||||
[email, passwordHash, name]
|
||||
);
|
||||
|
||||
const user = { id: result.insertId, email, name };
|
||||
const token = signUser(user);
|
||||
res.cookie("planlink_token", token, COOKIE_OPTS);
|
||||
return res.status(201).json({ ok: true, user });
|
||||
});
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
}
|
||||
|
||||
const user = await getOne(
|
||||
"SELECT id, email, name, password_hash FROM users WHERE email = ?",
|
||||
[email]
|
||||
);
|
||||
if (!user) {
|
||||
return res.status(401).json({ ok: false, message: "invalid credentials" });
|
||||
}
|
||||
|
||||
const valid = await verifyPassword(password, user.password_hash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ ok: false, message: "invalid credentials" });
|
||||
}
|
||||
|
||||
const token = signUser(user);
|
||||
res.cookie("planlink_token", token, COOKIE_OPTS);
|
||||
return res.json({ ok: true, user: { id: user.id, email: user.email, name: user.name } });
|
||||
});
|
||||
|
||||
router.post("/logout", (req, res) => {
|
||||
res.clearCookie("planlink_token", COOKIE_OPTS);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.get("/me", requireAuth, async (req, res) => {
|
||||
const user = await getOne("SELECT id, email, name, avatar_url FROM users WHERE id = ?", [req.user.id]);
|
||||
if (!user) return res.status(404).json({ ok: false, message: "not found" });
|
||||
res.json({ ok: true, user });
|
||||
});
|
||||
|
||||
// Google OAuth placeholders
|
||||
router.get("/google", (req, res) => {
|
||||
res.status(501).json({ ok: false, message: "google oauth not implemented" });
|
||||
});
|
||||
|
||||
router.get("/google/callback", (req, res) => {
|
||||
res.status(501).json({ ok: false, message: "google oauth callback not implemented" });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,80 @@
|
||||
const express = require("express");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function canAccessProject(userId, projectId) {
|
||||
const row = await getOne(
|
||||
"SELECT 1 FROM project_members WHERE project_id = ? AND user_id = ?",
|
||||
[projectId, userId]
|
||||
);
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
async function getProjectRole(userId, projectId) {
|
||||
const row = await getOne(
|
||||
"SELECT role FROM project_members WHERE project_id = ? AND user_id = ?",
|
||||
[projectId, userId]
|
||||
);
|
||||
return row?.role || null;
|
||||
}
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const projectId = req.query.projectId ? Number(req.query.projectId) : null;
|
||||
|
||||
let items;
|
||||
if (projectId) {
|
||||
const ok = await canAccessProject(req.user.id, projectId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
items = await query("SELECT * FROM boards WHERE project_id = ? ORDER BY position ASC", [projectId]);
|
||||
} else {
|
||||
items = await query(
|
||||
`SELECT b.*\n FROM boards b\n JOIN project_members pm ON pm.project_id = b.project_id\n WHERE pm.user_id = ?\n ORDER BY b.created_at DESC`,
|
||||
[req.user.id]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { projectId, name, description } = req.body || {};
|
||||
if (!projectId || !name) return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
|
||||
const ok = await canAccessProject(req.user.id, projectId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
const role = await getProjectRole(req.user.id, projectId);
|
||||
if (role === "member") {
|
||||
return res.status(403).json({ ok: false, message: "insufficient role" });
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO boards (project_id, name, description, position) VALUES (?, ?, ?, ?)",
|
||||
[projectId, name, description || null, 0]
|
||||
);
|
||||
const board = await getOne("SELECT * FROM boards WHERE id = ?", [result.insertId]);
|
||||
res.status(201).json({ ok: true, item: board });
|
||||
});
|
||||
|
||||
router.get("/:id/activity", requireAuth, async (req, res) => {
|
||||
const boardId = Number(req.params.id);
|
||||
const board = await getOne("SELECT project_id FROM boards WHERE id = ?", [boardId]);
|
||||
if (!board) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
const ok = await canAccessProject(req.user.id, board.project_id);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT a.id, a.type, a.payload, a.created_at, u.name as actor_name
|
||||
FROM activities a
|
||||
JOIN users u ON u.id = a.actor_id
|
||||
WHERE a.board_id = ?
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 200`,
|
||||
[boardId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,227 @@
|
||||
const express = require("express");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { emitBoard, logActivity } = require("../realtime/events");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function canAccessCard(userId, cardId) {
|
||||
const row = await getOne(
|
||||
`SELECT pm.user_id\n FROM cards c\n JOIN lists l ON l.id = c.list_id\n JOIN boards b ON b.id = l.board_id\n JOIN project_members pm ON pm.project_id = b.project_id\n WHERE c.id = ? AND pm.user_id = ?`,
|
||||
[cardId, userId]
|
||||
);
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
async function canAccessList(userId, listId) {
|
||||
const row = await getOne(
|
||||
`SELECT pm.user_id\n FROM lists l\n JOIN boards b ON b.id = l.board_id\n JOIN project_members pm ON pm.project_id = b.project_id\n WHERE l.id = ? AND pm.user_id = ?`,
|
||||
[listId, userId]
|
||||
);
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const listId = req.query.listId ? Number(req.query.listId) : null;
|
||||
if (!listId) return res.status(400).json({ ok: false, message: "listId required" });
|
||||
|
||||
const ok = await canAccessList(req.user.id, listId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
"SELECT * FROM cards WHERE list_id = ? AND archived_at IS NULL ORDER BY position ASC",
|
||||
[listId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { listId, title, description, position, dueAt } = req.body || {};
|
||||
if (!listId || !title) return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
|
||||
const ok = await canAccessList(req.user.id, listId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO cards (list_id, title, description, position, due_at, created_by) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[listId, title, description || null, Number(position || 0), dueAt || null, req.user.id]
|
||||
);
|
||||
const card = await getOne("SELECT * FROM cards WHERE id = ?", [result.insertId]);
|
||||
const board = await getOne(
|
||||
"SELECT b.id, b.project_id FROM lists l JOIN boards b ON b.id = l.board_id WHERE l.id = ?",
|
||||
[listId]
|
||||
);
|
||||
if (board) {
|
||||
emitBoard(board.id, "card:create", { card });
|
||||
await logActivity({
|
||||
projectId: board.project_id,
|
||||
boardId: board.id,
|
||||
cardId: card.id,
|
||||
actorId: req.user.id,
|
||||
type: "card.create",
|
||||
payload: { cardId: card.id }
|
||||
});
|
||||
}
|
||||
res.status(201).json({ ok: true, item: card });
|
||||
});
|
||||
|
||||
router.patch("/:id", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const { title, description, position, dueAt, listId } = req.body || {};
|
||||
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
if (listId) {
|
||||
const canMove = await canAccessList(req.user.id, listId);
|
||||
if (!canMove) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE cards\n SET title = COALESCE(?, title),\n description = COALESCE(?, description),\n position = COALESCE(?, position),\n due_at = COALESCE(?, due_at),\n list_id = COALESCE(?, list_id)\n WHERE id = ?`,
|
||||
[title || null, description || null, position ?? null, dueAt || null, listId || null, cardId]
|
||||
);
|
||||
|
||||
const updated = await getOne("SELECT * FROM cards WHERE id = ?", [cardId]);
|
||||
const board = await getOne(
|
||||
"SELECT b.id, b.project_id FROM cards c JOIN lists l ON l.id = c.list_id JOIN boards b ON b.id = l.board_id WHERE c.id = ?",
|
||||
[cardId]
|
||||
);
|
||||
if (board) {
|
||||
emitBoard(board.id, "card:update", { card: updated });
|
||||
await logActivity({
|
||||
projectId: board.project_id,
|
||||
boardId: board.id,
|
||||
cardId: cardId,
|
||||
actorId: req.user.id,
|
||||
type: "card.update",
|
||||
payload: { cardId }
|
||||
});
|
||||
}
|
||||
res.json({ ok: true, item: updated });
|
||||
});
|
||||
|
||||
router.post("/:id/members", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const { userId } = req.body || {};
|
||||
if (!userId) return res.status(400).json({ ok: false, message: "userId required" });
|
||||
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
await query(
|
||||
"INSERT IGNORE INTO card_members (card_id, user_id) VALUES (?, ?)",
|
||||
[cardId, userId]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.get("/:id/members", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT u.id, u.name, u.email
|
||||
FROM card_members cm
|
||||
JOIN users u ON u.id = cm.user_id
|
||||
WHERE cm.card_id = ?`,
|
||||
[cardId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/:id/labels", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const { labelId } = req.body || {};
|
||||
if (!labelId) return res.status(400).json({ ok: false, message: "labelId required" });
|
||||
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
await query(
|
||||
"INSERT IGNORE INTO card_labels (card_id, label_id) VALUES (?, ?)",
|
||||
[cardId, labelId]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.get("/:id/labels", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT l.id, l.name, l.color
|
||||
FROM card_labels cl
|
||||
JOIN labels l ON l.id = cl.label_id
|
||||
WHERE cl.card_id = ?`,
|
||||
[cardId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/:id/comments", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const { body } = req.body || {};
|
||||
if (!body) return res.status(400).json({ ok: false, message: "body required" });
|
||||
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO comments (card_id, user_id, body) VALUES (?, ?, ?)",
|
||||
[cardId, req.user.id, body]
|
||||
);
|
||||
res.status(201).json({ ok: true, commentId: result.insertId });
|
||||
});
|
||||
|
||||
router.get("/:id/comments", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT c.id, c.body, c.created_at, u.id as user_id, u.name as user_name
|
||||
FROM comments c
|
||||
JOIN users u ON u.id = c.user_id
|
||||
WHERE c.card_id = ?
|
||||
ORDER BY c.created_at ASC`,
|
||||
[cardId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/:id/attachments", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const { fileName, fileUrl, fileSize, mimeType } = req.body || {};
|
||||
if (!fileName || !fileUrl) {
|
||||
return res.status(400).json({ ok: false, message: "fileName and fileUrl required" });
|
||||
}
|
||||
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO attachments (card_id, user_id, file_name, file_url, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[cardId, req.user.id, fileName, fileUrl, fileSize || null, mimeType || null]
|
||||
);
|
||||
res.status(201).json({ ok: true, attachmentId: result.insertId });
|
||||
});
|
||||
|
||||
router.get("/:id/attachments", requireAuth, async (req, res) => {
|
||||
const cardId = Number(req.params.id);
|
||||
const ok = await canAccessCard(req.user.id, cardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT id, file_name, file_url, file_size, mime_type, created_at
|
||||
FROM attachments
|
||||
WHERE card_id = ?
|
||||
ORDER BY created_at DESC`,
|
||||
[cardId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,31 @@
|
||||
const express = require("express");
|
||||
const auth = require("./auth");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { getOne } = require("../db/queries");
|
||||
const projects = require("./projects");
|
||||
const boards = require("./boards");
|
||||
const lists = require("./lists");
|
||||
const cards = require("./cards");
|
||||
const planner = require("./planner");
|
||||
const labels = require("./labels");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/auth", auth);
|
||||
router.use("/projects", projects);
|
||||
router.use("/boards", boards);
|
||||
router.use("/lists", lists);
|
||||
router.use("/cards", cards);
|
||||
router.use("/planner", planner);
|
||||
router.use("/labels", labels);
|
||||
|
||||
router.get("/me", requireAuth, async (req, res) => {
|
||||
const user = await getOne(
|
||||
"SELECT id, email, name, avatar_url FROM users WHERE id = ?",
|
||||
[req.user.id]
|
||||
);
|
||||
if (!user) return res.status(404).json({ ok: false, message: "not found" });
|
||||
res.json({ ok: true, user });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,53 @@
|
||||
const express = require("express");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function canAccessProject(userId, projectId) {
|
||||
const row = await getOne(
|
||||
"SELECT 1 FROM project_members WHERE project_id = ? AND user_id = ?",
|
||||
[projectId, userId]
|
||||
);
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
async function getProjectRole(userId, projectId) {
|
||||
const row = await getOne(
|
||||
"SELECT role FROM project_members WHERE project_id = ? AND user_id = ?",
|
||||
[projectId, userId]
|
||||
);
|
||||
return row?.role || null;
|
||||
}
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const projectId = req.query.projectId ? Number(req.query.projectId) : null;
|
||||
if (!projectId) return res.status(400).json({ ok: false, message: "projectId required" });
|
||||
|
||||
const ok = await canAccessProject(req.user.id, projectId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
const role = await getProjectRole(req.user.id, projectId);
|
||||
if (role === "member") {
|
||||
return res.status(403).json({ ok: false, message: "insufficient role" });
|
||||
}
|
||||
|
||||
const items = await query("SELECT * FROM labels WHERE project_id = ?", [projectId]);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { projectId, name, color } = req.body || {};
|
||||
if (!projectId || !name) return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
|
||||
const ok = await canAccessProject(req.user.id, projectId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO labels (project_id, name, color) VALUES (?, ?, ?)",
|
||||
[projectId, name, color || "slate"]
|
||||
);
|
||||
const label = await getOne("SELECT * FROM labels WHERE id = ?", [result.insertId]);
|
||||
res.status(201).json({ ok: true, item: label });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,108 @@
|
||||
const express = require("express");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { emitBoard, logActivity } = require("../realtime/events");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function canAccessBoard(userId, boardId) {
|
||||
const row = await getOne(
|
||||
`SELECT pm.user_id\n FROM boards b\n JOIN project_members pm ON pm.project_id = b.project_id\n WHERE b.id = ? AND pm.user_id = ?`,
|
||||
[boardId, userId]
|
||||
);
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
async function getRoleByBoard(userId, boardId) {
|
||||
const row = await getOne(
|
||||
`SELECT pm.role\n FROM boards b\n JOIN project_members pm ON pm.project_id = b.project_id\n WHERE b.id = ? AND pm.user_id = ?`,
|
||||
[boardId, userId]
|
||||
);
|
||||
return row?.role || null;
|
||||
}
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const boardId = req.query.boardId ? Number(req.query.boardId) : null;
|
||||
if (!boardId) return res.status(400).json({ ok: false, message: "boardId required" });
|
||||
|
||||
const ok = await canAccessBoard(req.user.id, boardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
const role = await getRoleByBoard(req.user.id, boardId);
|
||||
if (role === "member") {
|
||||
return res.status(403).json({ ok: false, message: "insufficient role" });
|
||||
}
|
||||
|
||||
const items = await query("SELECT * FROM lists WHERE board_id = ? ORDER BY position ASC", [boardId]);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { boardId, name, position } = req.body || {};
|
||||
if (!boardId || !name) return res.status(400).json({ ok: false, message: "missing fields" });
|
||||
|
||||
const ok = await canAccessBoard(req.user.id, boardId);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO lists (board_id, name, position) VALUES (?, ?, ?)",
|
||||
[boardId, name, Number(position || 0)]
|
||||
);
|
||||
const list = await getOne("SELECT * FROM lists WHERE id = ?", [result.insertId]);
|
||||
const board = await getOne("SELECT project_id FROM boards WHERE id = ?", [boardId]);
|
||||
emitBoard(boardId, "list:create", { list });
|
||||
if (board) {
|
||||
await logActivity({
|
||||
projectId: board.project_id,
|
||||
boardId,
|
||||
actorId: req.user.id,
|
||||
type: "list.create",
|
||||
payload: { listId: list.id }
|
||||
});
|
||||
}
|
||||
res.status(201).json({ ok: true, item: list });
|
||||
});
|
||||
|
||||
router.patch("/:id", requireAuth, async (req, res) => {
|
||||
const listId = Number(req.params.id);
|
||||
const { name, position } = req.body || {};
|
||||
|
||||
const list = await getOne(
|
||||
`SELECT l.id, l.board_id\n FROM lists l\n WHERE l.id = ?`,
|
||||
[listId]
|
||||
);
|
||||
if (!list) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
const ok = await canAccessBoard(req.user.id, list.board_id);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
const role = await getRoleByBoard(req.user.id, list.board_id);
|
||||
if (role === "member") {
|
||||
return res.status(403).json({ ok: false, message: "insufficient role" });
|
||||
}
|
||||
|
||||
await query(
|
||||
"UPDATE lists SET name = COALESCE(?, name), position = COALESCE(?, position) WHERE id = ?",
|
||||
[name || null, position ?? null, listId]
|
||||
);
|
||||
const updated = await getOne("SELECT * FROM lists WHERE id = ?", [listId]);
|
||||
emitBoard(list.board_id, "list:update", { list: updated });
|
||||
res.json({ ok: true, item: updated });
|
||||
});
|
||||
|
||||
router.delete("/:id", requireAuth, async (req, res) => {
|
||||
const listId = Number(req.params.id);
|
||||
const list = await getOne("SELECT id, board_id FROM lists WHERE id = ?", [listId]);
|
||||
if (!list) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
const ok = await canAccessBoard(req.user.id, list.board_id);
|
||||
if (!ok) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
const role = await getRoleByBoard(req.user.id, list.board_id);
|
||||
if (role === "member") {
|
||||
return res.status(403).json({ ok: false, message: "insufficient role" });
|
||||
}
|
||||
|
||||
await query("DELETE FROM lists WHERE id = ?", [listId]);
|
||||
emitBoard(list.board_id, "list:delete", { listId });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,190 @@
|
||||
const express = require("express");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { renderTemplate } = require("../services/plannerTemplates");
|
||||
const { getOpenAI } = require("../services/openai");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/requests", requireAuth, async (req, res) => {
|
||||
const { projectId, inputText, mode, templateKey } = req.body || {};
|
||||
if (!inputText || !mode) {
|
||||
return res.status(400).json({ ok: false, message: "inputText and mode required" });
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO planner_requests (project_id, user_id, input_text, mode, template_key) VALUES (?, ?, ?, ?, ?)",
|
||||
[projectId || null, req.user.id, inputText, mode, templateKey || null]
|
||||
);
|
||||
|
||||
res.status(201).json({ ok: true, requestId: result.insertId, status: "queued" });
|
||||
});
|
||||
|
||||
// Guardar un planner manual pegado desde Codex/ChatGPT
|
||||
router.post("/manual", requireAuth, async (req, res) => {
|
||||
const { projectId, outputText, title } = req.body || {};
|
||||
if (!outputText) {
|
||||
return res.status(400).json({ ok: false, message: "outputText required" });
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO planner_requests (project_id, user_id, input_text, mode, status) VALUES (?, ?, ?, 'template', 'done')",
|
||||
[projectId || null, req.user.id, title || "Manual planner"]
|
||||
);
|
||||
|
||||
await query(
|
||||
"INSERT INTO planner_outputs (request_id, output_text) VALUES (?, ?)",
|
||||
[result.insertId, outputText]
|
||||
);
|
||||
|
||||
res.status(201).json({ ok: true, requestId: result.insertId });
|
||||
});
|
||||
|
||||
router.get("/requests/:id", requireAuth, async (req, res) => {
|
||||
const requestId = Number(req.params.id);
|
||||
const reqRow = await getOne(
|
||||
"SELECT * FROM planner_requests WHERE id = ? AND user_id = ?",
|
||||
[requestId, req.user.id]
|
||||
);
|
||||
if (!reqRow) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
const output = await getOne(
|
||||
"SELECT * FROM planner_outputs WHERE request_id = ?",
|
||||
[requestId]
|
||||
);
|
||||
res.json({ ok: true, request: reqRow, output });
|
||||
});
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const items = await query(
|
||||
`SELECT pr.id, pr.project_id, pr.input_text, pr.mode, pr.status, pr.created_at,
|
||||
po.output_text
|
||||
FROM planner_requests pr
|
||||
LEFT JOIN planner_outputs po ON po.request_id = pr.id
|
||||
WHERE pr.user_id = ?
|
||||
ORDER BY pr.created_at DESC
|
||||
LIMIT 200`,
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.patch("/requests/:id", requireAuth, async (req, res) => {
|
||||
const requestId = Number(req.params.id);
|
||||
const { outputText } = req.body || {};
|
||||
if (!outputText) return res.status(400).json({ ok: false, message: "outputText required" });
|
||||
|
||||
const reqRow = await getOne(
|
||||
"SELECT id FROM planner_requests WHERE id = ? AND user_id = ?",
|
||||
[requestId, req.user.id]
|
||||
);
|
||||
if (!reqRow) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
const existing = await getOne(
|
||||
"SELECT id FROM planner_outputs WHERE request_id = ?",
|
||||
[requestId]
|
||||
);
|
||||
if (existing) {
|
||||
await query("UPDATE planner_outputs SET output_text = ? WHERE request_id = ?", [
|
||||
outputText,
|
||||
requestId
|
||||
]);
|
||||
} else {
|
||||
await query("INSERT INTO planner_outputs (request_id, output_text) VALUES (?, ?)", [
|
||||
requestId,
|
||||
outputText
|
||||
]);
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post("/requests/:id/run", requireAuth, async (req, res) => {
|
||||
const requestId = Number(req.params.id);
|
||||
const reqRow = await getOne(
|
||||
"SELECT * FROM planner_requests WHERE id = ? AND user_id = ?",
|
||||
[requestId, req.user.id]
|
||||
);
|
||||
if (!reqRow) return res.status(404).json({ ok: false, message: "not found" });
|
||||
|
||||
if (reqRow.mode === "template") {
|
||||
await query("UPDATE planner_requests SET status = 'running' WHERE id = ?", [requestId]);
|
||||
const outputText = renderTemplate(reqRow.template_key || "default", reqRow.input_text);
|
||||
await query(
|
||||
"INSERT INTO planner_outputs (request_id, output_text) VALUES (?, ?)",
|
||||
[requestId, outputText]
|
||||
);
|
||||
await query("UPDATE planner_requests SET status = 'done' WHERE id = ?", [requestId]);
|
||||
return res.json({ ok: true, id: requestId, status: "done", outputText });
|
||||
}
|
||||
|
||||
// IA: ejecutar via streaming en endpoint dedicado
|
||||
await query("UPDATE planner_requests SET status = 'queued' WHERE id = ?", [requestId]);
|
||||
return res.json({ ok: true, id: requestId, status: "queued", message: "use /stream" });
|
||||
});
|
||||
|
||||
router.get("/requests/:id/stream", requireAuth, async (req, res) => {
|
||||
const requestId = Number(req.params.id);
|
||||
const reqRow = await getOne(
|
||||
"SELECT * FROM planner_requests WHERE id = ? AND user_id = ?",
|
||||
[requestId, req.user.id]
|
||||
);
|
||||
if (!reqRow) return res.status(404).json({ ok: false, message: "not found" });
|
||||
if (reqRow.mode !== "ai") {
|
||||
return res.status(400).json({ ok: false, message: "mode must be ai" });
|
||||
}
|
||||
if (process.env.AI_ENABLED !== "true") {
|
||||
return res.status(403).json({ ok: false, message: "AI disabled" });
|
||||
}
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
return res.status(400).json({ ok: false, message: "OPENAI_API_KEY missing" });
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.flushHeaders();
|
||||
|
||||
await query("UPDATE planner_requests SET status = 'running' WHERE id = ?", [requestId]);
|
||||
|
||||
let fullText = "";
|
||||
try {
|
||||
const client = getOpenAI();
|
||||
const stream = await client.responses.create({
|
||||
model: process.env.OPENAI_MODEL || "gpt-4o-mini",
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Eres un asistente que genera planers para proyectos estilo kanban. Devuelve un plan claro en Markdown con secciones: Objetivo, Alcance, Lista de tareas, Entregables."
|
||||
},
|
||||
{ role: "user", content: reqRow.input_text }
|
||||
],
|
||||
stream: true
|
||||
});
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "response.output_text.delta") {
|
||||
fullText += event.delta;
|
||||
res.write(`event: delta\\n`);
|
||||
res.write(`data: ${JSON.stringify({ delta: event.delta })}\\n\\n`);
|
||||
}
|
||||
}
|
||||
|
||||
await query(
|
||||
"INSERT INTO planner_outputs (request_id, output_text) VALUES (?, ?)",
|
||||
[requestId, fullText]
|
||||
);
|
||||
await query("UPDATE planner_requests SET status = 'done' WHERE id = ?", [requestId]);
|
||||
res.write(`event: done\\n`);
|
||||
res.write(`data: ${JSON.stringify({ ok: true })}\\n\\n`);
|
||||
res.end();
|
||||
} catch (err) {
|
||||
await query("UPDATE planner_requests SET status = 'failed' WHERE id = ?", [requestId]);
|
||||
res.write(`event: error\\n`);
|
||||
res.write(`data: ${JSON.stringify({ message: err.message })}\\n\\n`);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,145 @@
|
||||
const express = require("express");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { query, getOne } = require("../db/queries");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function getProjectRole(userId, projectId) {
|
||||
const row = await getOne(
|
||||
"SELECT role FROM project_members WHERE project_id = ? AND user_id = ?",
|
||||
[projectId, userId]
|
||||
);
|
||||
return row?.role || null;
|
||||
}
|
||||
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
const items = await query(
|
||||
`SELECT p.*,\n pm.role\n FROM projects p\n JOIN project_members pm ON pm.project_id = p.id\n WHERE pm.user_id = ?\n ORDER BY p.created_at DESC`,
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { name, description } = req.body || {};
|
||||
if (!name) return res.status(400).json({ ok: false, message: "name required" });
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO projects (name, description, owner_id) VALUES (?, ?, ?)",
|
||||
[name, description || null, req.user.id]
|
||||
);
|
||||
await query(
|
||||
"INSERT INTO project_members (project_id, user_id, role) VALUES (?, ?, 'owner')",
|
||||
[result.insertId, req.user.id]
|
||||
);
|
||||
|
||||
const project = await getOne("SELECT * FROM projects WHERE id = ?", [result.insertId]);
|
||||
res.status(201).json({ ok: true, item: project });
|
||||
});
|
||||
|
||||
router.post("/:id/invites", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const { email, role } = req.body || {};
|
||||
if (!email) return res.status(400).json({ ok: false, message: "email required" });
|
||||
|
||||
const currentRole = await getProjectRole(req.user.id, projectId);
|
||||
if (!currentRole || (currentRole !== "owner" && currentRole !== "admin")) {
|
||||
return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
}
|
||||
|
||||
const token = uuidv4().replace(/-/g, "");
|
||||
const result = await query(
|
||||
"INSERT INTO project_invites (project_id, email, role, token, invited_by) VALUES (?, ?, ?, ?, ?)",
|
||||
[projectId, email, role === "admin" ? "admin" : "member", token, req.user.id]
|
||||
);
|
||||
|
||||
res.status(201).json({ ok: true, inviteId: result.insertId, token, email, role: role === "admin" ? "admin" : "member" });
|
||||
});
|
||||
|
||||
router.get("/:id/invites", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const role = await getProjectRole(req.user.id, projectId);
|
||||
if (!role || (role !== "owner" && role !== "admin")) {
|
||||
return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
}
|
||||
|
||||
const items = await query(
|
||||
"SELECT id, email, role, status, created_at FROM project_invites WHERE project_id = ? ORDER BY created_at DESC",
|
||||
[projectId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/:id/members", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const { userId, role } = req.body || {};
|
||||
if (!userId) return res.status(400).json({ ok: false, message: "userId required" });
|
||||
|
||||
const currentRole = await getProjectRole(req.user.id, projectId);
|
||||
if (!currentRole || currentRole !== "owner") {
|
||||
return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
}
|
||||
|
||||
await query(
|
||||
"INSERT INTO project_members (project_id, user_id, role) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE role = VALUES(role)",
|
||||
[projectId, userId, role === "admin" ? "admin" : "member"]
|
||||
);
|
||||
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.get("/:id/members", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const role = await getProjectRole(req.user.id, projectId);
|
||||
if (!role) return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
|
||||
const items = await query(
|
||||
`SELECT u.id, u.name, u.email, pm.role
|
||||
FROM project_members pm
|
||||
JOIN users u ON u.id = pm.user_id
|
||||
WHERE pm.project_id = ?
|
||||
ORDER BY u.name ASC`,
|
||||
[projectId]
|
||||
);
|
||||
res.json({ ok: true, items });
|
||||
});
|
||||
|
||||
router.post("/:id/invites/accept", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const { token } = req.body || {};
|
||||
if (!token) return res.status(400).json({ ok: false, message: "token required" });
|
||||
|
||||
const invite = await getOne(
|
||||
"SELECT * FROM project_invites WHERE project_id = ? AND token = ? AND status = 'pending'",
|
||||
[projectId, token]
|
||||
);
|
||||
if (!invite) return res.status(404).json({ ok: false, message: "invite not found" });
|
||||
|
||||
await query(
|
||||
"INSERT INTO project_members (project_id, user_id, role) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE role = VALUES(role)",
|
||||
[projectId, req.user.id, invite.role]
|
||||
);
|
||||
await query("UPDATE project_invites SET status = 'accepted' WHERE id = ?", [invite.id]);
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post("/:id/invites/revoke", requireAuth, async (req, res) => {
|
||||
const projectId = Number(req.params.id);
|
||||
const { inviteId } = req.body || {};
|
||||
if (!inviteId) return res.status(400).json({ ok: false, message: "inviteId required" });
|
||||
|
||||
const role = await getProjectRole(req.user.id, projectId);
|
||||
if (!role || (role !== "owner" && role !== "admin")) {
|
||||
return res.status(403).json({ ok: false, message: "forbidden" });
|
||||
}
|
||||
|
||||
await query(
|
||||
"UPDATE project_invites SET status = 'revoked' WHERE id = ? AND project_id = ?",
|
||||
[inviteId, projectId]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,12 @@
|
||||
const OpenAI = require("openai");
|
||||
|
||||
let client;
|
||||
|
||||
function getOpenAI() {
|
||||
if (!client) {
|
||||
client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
module.exports = { getOpenAI };
|
||||
@@ -0,0 +1,15 @@
|
||||
const templates = {
|
||||
default: ({ input }) => {
|
||||
return `# Plan\n\n## Objetivo\n${input}\n\n## Pasos\n1. Definir alcance\n2. Crear tareas por lista\n3. Asignar responsables\n4. Estimar tiempos\n5. Revisar y ajustar\n`;
|
||||
},
|
||||
lanzamiento: ({ input }) => {
|
||||
return `# Plan de lanzamiento\n\n## Objetivo\n${input}\n\n## Checklist\n- Definir fecha\n- Crear tablero\n- Asignar tareas\n- QA\n- Publicacion\n`;
|
||||
}
|
||||
};
|
||||
|
||||
function renderTemplate(key, input) {
|
||||
const tpl = templates[key] || templates.default;
|
||||
return tpl({ input });
|
||||
}
|
||||
|
||||
module.exports = { renderTemplate };
|
||||
@@ -0,0 +1,11 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
function signUser(user) {
|
||||
return jwt.sign(
|
||||
{ id: user.id, email: user.email, name: user.name },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { signUser };
|
||||
@@ -0,0 +1,13 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
async function hashPassword(password) {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
async function verifyPassword(password, hash) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
module.exports = { hashPassword, verifyPassword };
|
||||
Reference in New Issue
Block a user