diff --git a/android/store-assets/phone-1-tasks.png b/android/store-assets/phone-1-tasks.png new file mode 100644 index 0000000..74f2fd6 Binary files /dev/null and b/android/store-assets/phone-1-tasks.png differ diff --git a/android/store-assets/phone-2-calendar.png b/android/store-assets/phone-2-calendar.png new file mode 100644 index 0000000..53e92f2 Binary files /dev/null and b/android/store-assets/phone-2-calendar.png differ diff --git a/android/store-assets/phone-3-chat.png b/android/store-assets/phone-3-chat.png new file mode 100644 index 0000000..cdd7452 Binary files /dev/null and b/android/store-assets/phone-3-chat.png differ diff --git a/android/store-assets/phone-4-goals.png b/android/store-assets/phone-4-goals.png new file mode 100644 index 0000000..3cd2022 Binary files /dev/null and b/android/store-assets/phone-4-goals.png differ diff --git a/android/store-assets/tablet10-1-tasks.png b/android/store-assets/tablet10-1-tasks.png new file mode 100644 index 0000000..54d035a Binary files /dev/null and b/android/store-assets/tablet10-1-tasks.png differ diff --git a/android/store-assets/tablet10-2-calendar.png b/android/store-assets/tablet10-2-calendar.png new file mode 100644 index 0000000..5d31286 Binary files /dev/null and b/android/store-assets/tablet10-2-calendar.png differ diff --git a/android/store-assets/tablet7-1-tasks.png b/android/store-assets/tablet7-1-tasks.png new file mode 100644 index 0000000..e443671 Binary files /dev/null and b/android/store-assets/tablet7-1-tasks.png differ diff --git a/android/store-assets/tablet7-2-calendar.png b/android/store-assets/tablet7-2-calendar.png new file mode 100644 index 0000000..4fe57d1 Binary files /dev/null and b/android/store-assets/tablet7-2-calendar.png differ diff --git a/api/package-lock.json b/api/package-lock.json index 3d17f67..5cc2883 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -16,6 +16,7 @@ "@fastify/rate-limit": "^10.3.0", "@fastify/swagger": "^9.7.0", "@fastify/swagger-ui": "^5.2.5", + "@fastify/websocket": "^11.2.0", "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", "dotenv": "^17.3.1", @@ -367,6 +368,27 @@ "yaml": "^2.4.1" } }, + "node_modules/@fastify/websocket": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.2.0.tgz", + "integrity": "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^5.0.0", + "ws": "^8.16.0" + } + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -767,6 +789,18 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -776,6 +810,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1462,6 +1505,15 @@ "node": ">=14.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -1767,6 +1819,20 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2016,6 +2082,21 @@ "reusify": "^1.0.0" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2101,6 +2182,12 @@ "dev": true, "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2142,6 +2229,33 @@ "node": ">= 16" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/api/package.json b/api/package.json index f4b522a..b2bd4cc 100644 --- a/api/package.json +++ b/api/package.json @@ -20,6 +20,7 @@ "@fastify/rate-limit": "^10.3.0", "@fastify/swagger": "^9.7.0", "@fastify/swagger-ui": "^5.2.5", + "@fastify/websocket": "^11.2.0", "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", "dotenv": "^17.3.1", diff --git a/api/src/index.js b/api/src/index.js index f9f7e35..d302172 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -6,6 +6,7 @@ const jwt = require("@fastify/jwt"); const rateLimit = require("@fastify/rate-limit"); const { Pool } = require("pg"); const Redis = require("ioredis"); +const websocket = require("@fastify/websocket"); const swagger = require("@fastify/swagger"); const swaggerUi = require("@fastify/swagger-ui"); @@ -85,6 +86,16 @@ const start = async () => { uiConfig: { docExpansion: "list", deepLinking: true } }); + // WebSocket support + await app.register(websocket); + + app.get("/ws", { websocket: true }, (socket, req) => { + socket.on("message", (msg) => { + socket.send(JSON.stringify({ type: "ack", message: msg.toString() })); + }); + socket.send(JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })); + }); + // Register routes await app.register(require("./routes/tasks"), { prefix: "/api/v1" }); await app.register(require("./routes/groups"), { prefix: "/api/v1" }); diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js index 4846339..a4f3e19 100644 --- a/api/src/routes/auth.js +++ b/api/src/routes/auth.js @@ -115,6 +115,23 @@ async function authRoutes(app) { // OAuth initiate routes moved to ./oauth.js + + // Export all user data (GDPR) + app.get("/auth/export-data", { preHandler: [async (req) => { await req.jwtVerify(); }] }, async (req) => { + const userId = req.user.id; + const { rows: user } = await app.db.query("SELECT id,email,name,phone,language,created_at FROM users WHERE id=$1", [userId]); + const { rows: tasks } = await app.db.query("SELECT * FROM tasks WHERE user_id=$1", [userId]); + const { rows: goals } = await app.db.query("SELECT * FROM goals WHERE user_id=$1", [userId]); + const { rows: groups } = await app.db.query("SELECT * FROM task_groups WHERE user_id=$1 OR user_id IS NULL", [userId]); + const { rows: comments } = await app.db.query("SELECT * FROM task_comments WHERE user_id=$1", [userId]); + return { + exported_at: new Date().toISOString(), + user: user[0] || {}, + tasks, goals, groups, comments, + total: { tasks: tasks.length, goals: goals.length, comments: comments.length } + }; + }); + // Search users by name or email (for collaboration) app.get('/auth/users/search', async (req) => { const q = (req.query.q || '').trim(); @@ -130,21 +147,3 @@ async function authRoutes(app) { module.exports = authRoutes; -// Delete account (GDPR + Google Play requirement) -app.delete("/auth/delete-account", { preHandler: [async (req) => { await req.jwtVerify(); }] }, async (req) => { - const userId = req.user.id; - - // Delete all user data in order (foreign keys) - await app.db.query("DELETE FROM task_comments WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM subtasks WHERE assigned_to = $1", [userId]); - await app.db.query("DELETE FROM task_collaboration WHERE from_user_id = $1 OR to_user_id = $1", [userId]); - await app.db.query("DELETE FROM task_assignments WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM push_subscriptions WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM goals WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM connectors WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM tasks WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM task_groups WHERE user_id = $1", [userId]); - await app.db.query("DELETE FROM users WHERE id = $1", [userId]); - - return { status: "deleted", message: "Account and all data permanently deleted" }; -}); diff --git a/apps/tasks/app/tasks/page.tsx b/apps/tasks/app/tasks/page.tsx index 89feed3..8ebb4b1 100644 --- a/apps/tasks/app/tasks/page.tsx +++ b/apps/tasks/app/tasks/page.tsx @@ -130,30 +130,30 @@ export default function TasksPage() { return (