Swagger docs + Error tracking + Email + Production keystore

- Swagger UI at /docs (70+ routes documented)
- Error tracking: error_logs table, /errors/report, /errors/recent, /errors/stats
- Global error handler logs to DB
- Email: nodemailer, /email/send, /email/reminder (SMTP placeholder)
- Production keystore: RSA 2048, valid 2053, info.hasdo.taskteam

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 18:27:33 +00:00
parent 6089f4d310
commit 070a62bdf1
7 changed files with 154 additions and 0 deletions

55
api/src/routes/email.js Normal file
View File

@@ -0,0 +1,55 @@
// Task Team — Email Notifications — 2026-03-29
const nodemailer = require("nodemailer");
async function emailRoutes(app) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || "smtp.gmail.com",
port: parseInt(process.env.SMTP_PORT || "587"),
secure: false,
auth: {
user: process.env.SMTP_USER || "",
pass: process.env.SMTP_PASS || ""
}
});
// Send email
app.post("/email/send", async (req) => {
const { to, subject, text, html } = req.body;
if (!to || !subject) throw { statusCode: 400, message: "to and subject required" };
if (!process.env.SMTP_USER) {
return { status: "skipped", message: "SMTP not configured" };
}
const info = await transporter.sendMail({
from: process.env.SMTP_FROM || '"Task Team" <noreply@hasdo.info>',
to, subject, text, html
});
return { status: "sent", messageId: info.messageId };
});
// Send task reminder
app.post("/email/reminder", async (req) => {
const { user_id, task_id } = req.body;
const { rows: users } = await app.db.query("SELECT email, name FROM users WHERE id=$1", [user_id]);
const { rows: tasks } = await app.db.query("SELECT title, due_at FROM tasks WHERE id=$1", [task_id]);
if (!users.length || !tasks.length) throw { statusCode: 404, message: "User or task not found" };
if (!process.env.SMTP_USER) return { status: "skipped", message: "SMTP not configured" };
await transporter.sendMail({
from: '"Task Team" <noreply@hasdo.info>',
to: users[0].email,
subject: `Pripominka: ${tasks[0].title}`,
html: `<h2>Pripominka ukolu</h2><p><strong>${tasks[0].title}</strong></p><p>Termin: ${tasks[0].due_at || "bez terminu"}</p><p><a href="https://tasks.hasdo.info/tasks">Otevrit v Task Team</a></p>`
});
return { status: "sent" };
});
// Email config status
app.get("/email/status", async () => {
return { configured: !!process.env.SMTP_USER, host: process.env.SMTP_HOST || "not set" };
});
}
module.exports = emailRoutes;

55
api/src/routes/errors.js Normal file
View File

@@ -0,0 +1,55 @@
// Task Team — Error Tracking — 2026-03-29
async function errorRoutes(app) {
CREATE INDEX IF NOT EXISTS idx_errors_created ON error_logs(created_at DESC);
`);
// Log error from client
app.post("/errors/report", async (req) => {
const { message, stack, url, metadata } = req.body;
await app.db.query(
"INSERT INTO error_logs (message, stack, url, method, metadata) VALUES ($1,$2,$3,$4,$5)",
[message || "Unknown error", stack || "", url || "", req.method, JSON.stringify(metadata || {})]
);
return { status: "logged" };
});
// List recent errors (admin)
app.get("/errors/recent", async (req) => {
const { rows } = await app.db.query(
"SELECT * FROM error_logs ORDER BY created_at DESC LIMIT 50"
);
return { data: rows };
});
// Error stats
app.get("/errors/stats", async (req) => {
const { rows } = await app.db.query(`
SELECT
count(*) as total,
count(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as last_hour,
count(*) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours') as last_day
FROM error_logs
`);
return { data: rows[0] };
});
// Global error handler for the API itself
app.setErrorHandler(async (error, request, reply) => {
app.log.error(error);
try {
await app.db.query(
"INSERT INTO error_logs (level, message, stack, url, method) VALUES ($1,$2,$3,$4,$5)",
[error.statusCode >= 500 ? "error" : "warn", error.message, error.stack || "", request.url, request.method]
);
} catch (e) {
// Silent catch - error logging should not break the response
}
reply.status(error.statusCode || 500).send({
error: error.message || "Internal Server Error",
statusCode: error.statusCode || 500
});
});
}
module.exports = errorRoutes;