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:
10
api/package-lock.json
generated
10
api/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"fastify": "^5.8.4",
|
||||
"ioredis": "^5.10.1",
|
||||
"nodemailer": "^8.0.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
@@ -1392,6 +1393,15 @@
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"fastify": "^5.8.4",
|
||||
"ioredis": "^5.10.1",
|
||||
"nodemailer": "^8.0.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
|
||||
@@ -6,6 +6,8 @@ const jwt = require("@fastify/jwt");
|
||||
const rateLimit = require("@fastify/rate-limit");
|
||||
const { Pool } = require("pg");
|
||||
const Redis = require("ioredis");
|
||||
const swagger = require("@fastify/swagger");
|
||||
const swaggerUi = require("@fastify/swagger-ui");
|
||||
|
||||
// Database pool
|
||||
const pool = new Pool({
|
||||
@@ -58,6 +60,31 @@ const start = async () => {
|
||||
redis: redis.status
|
||||
}));
|
||||
|
||||
|
||||
// Swagger/OpenAPI documentation
|
||||
await app.register(swagger, {
|
||||
openapi: {
|
||||
info: { title: "Task Team API", version: "1.0.0", description: "Personal life management system API" },
|
||||
servers: [{ url: "https://api.hasdo.info" }],
|
||||
tags: [
|
||||
{ name: "Auth", description: "Authentication & OAuth" },
|
||||
{ name: "Tasks", description: "Task management" },
|
||||
{ name: "Groups", description: "Task groups" },
|
||||
{ name: "Goals", description: "Goals & AI planner" },
|
||||
{ name: "Projects", description: "Project management" },
|
||||
{ name: "Collaboration", description: "Team collaboration" },
|
||||
{ name: "Connectors", description: "External integrations" },
|
||||
{ name: "Notifications", description: "Push notifications" },
|
||||
{ name: "System", description: "Health & monitoring" }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await app.register(swaggerUi, {
|
||||
routePrefix: "/docs",
|
||||
uiConfig: { docExpansion: "list", deepLinking: true }
|
||||
});
|
||||
|
||||
// Register routes
|
||||
await app.register(require("./routes/tasks"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/groups"), { prefix: "/api/v1" });
|
||||
@@ -74,6 +101,8 @@ const start = async () => {
|
||||
await app.register(require("./routes/deploy"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/system"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/collaboration"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/email"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/errors"), { prefix: "/api/v1" });
|
||||
|
||||
try {
|
||||
await app.listen({ port: process.env.PORT || 3000, host: "0.0.0.0" });
|
||||
|
||||
55
api/src/routes/email.js
Normal file
55
api/src/routes/email.js
Normal 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
55
api/src/routes/errors.js
Normal 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;
|
||||
4
mobile/keystore.properties
Normal file
4
mobile/keystore.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
storeFile=../../task-team-release.keystore
|
||||
storePassword=TaskTeam2026Release!
|
||||
keyAlias=task-team
|
||||
keyPassword=TaskTeam2026Release!
|
||||
BIN
mobile/task-team-release.keystore
Normal file
BIN
mobile/task-team-release.keystore
Normal file
Binary file not shown.
Reference in New Issue
Block a user