From 070a62bdf1baf775bdd0cbd30db89af0c4f40e44 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 29 Mar 2026 18:27:33 +0000 Subject: [PATCH] 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) --- api/package-lock.json | 10 ++++++ api/package.json | 1 + api/src/index.js | 29 ++++++++++++++++ api/src/routes/email.js | 55 ++++++++++++++++++++++++++++++ api/src/routes/errors.js | 55 ++++++++++++++++++++++++++++++ mobile/keystore.properties | 4 +++ mobile/task-team-release.keystore | Bin 0 -> 2798 bytes 7 files changed, 154 insertions(+) create mode 100644 api/src/routes/email.js create mode 100644 api/src/routes/errors.js create mode 100644 mobile/keystore.properties create mode 100644 mobile/task-team-release.keystore diff --git a/api/package-lock.json b/api/package-lock.json index c0de017..3d17f67 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -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", diff --git a/api/package.json b/api/package.json index 47e1ffd..f4b522a 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/src/index.js b/api/src/index.js index 0919f3d..f9f7e35 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -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" }); diff --git a/api/src/routes/email.js b/api/src/routes/email.js new file mode 100644 index 0000000..12018c1 --- /dev/null +++ b/api/src/routes/email.js @@ -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" ', + 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" ', + to: users[0].email, + subject: `Pripominka: ${tasks[0].title}`, + html: `

Pripominka ukolu

${tasks[0].title}

Termin: ${tasks[0].due_at || "bez terminu"}

Otevrit v Task Team

` + }); + 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; diff --git a/api/src/routes/errors.js b/api/src/routes/errors.js new file mode 100644 index 0000000..8bcb919 --- /dev/null +++ b/api/src/routes/errors.js @@ -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; diff --git a/mobile/keystore.properties b/mobile/keystore.properties new file mode 100644 index 0000000..cc4332c --- /dev/null +++ b/mobile/keystore.properties @@ -0,0 +1,4 @@ +storeFile=../../task-team-release.keystore +storePassword=TaskTeam2026Release! +keyAlias=task-team +keyPassword=TaskTeam2026Release! diff --git a/mobile/task-team-release.keystore b/mobile/task-team-release.keystore new file mode 100644 index 0000000000000000000000000000000000000000..e11736fa74e047d18f616a44aa0c82b274aa1179 GIT binary patch literal 2798 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 ZVqH3WIhg(tcd5gNW!bT*HQ}F;^$+>T8vXzP literal 0 HcmV?d00001