diff --git a/package-lock.json b/package-lock.json index 1605b50..7aa2c58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "dependencies": { "@auth/prisma-adapter": "^2.10.0", "@prisma/client": "^6.14.0", + "mongodb": "^7.0.0", "next": "15.4.6", - "next-auth": "^4.24.11", + "next-auth": "^4.24.13", "prisma": "^6.14.0", "react": "19.1.0", "react-dom": "19.1.0" @@ -19,6 +20,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/mongodb": "^4.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -41,87 +43,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@auth/core": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz", - "integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "@panva/hkdf": "^1.1.1", - "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.10.4", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^6.8.0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/@auth/core/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@auth/core/node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/@auth/core/node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/@auth/core/node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/@auth/prisma-adapter": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.10.0.tgz", @@ -186,6 +107,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -930,6 +852,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1150,6 +1081,7 @@ "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -1539,14 +1471,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1568,6 +1492,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mongodb": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-4.0.6.tgz", + "integrity": "sha512-XTbn1Z1j7fHzC1Vkd9LYO48lO2C581r+oRCi/KNzcTHIri7hEaya8r9vxoHJiKr+oeUWVK69+9xr84Mp+aReaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mongodb": "*" + } + }, "node_modules/@types/node": { "version": "20.19.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", @@ -1584,6 +1518,7 @@ "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1598,6 +1533,21 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.39.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", @@ -1644,6 +1594,7 @@ "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", @@ -2161,6 +2112,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2472,6 +2424,15 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -3164,6 +3125,7 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3338,6 +3300,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4982,6 +4945,12 @@ "node": ">= 0.4" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5068,6 +5037,65 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5169,9 +5197,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -5185,9 +5213,9 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -5259,17 +5287,6 @@ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", "license": "MIT" }, - "node_modules/oauth4webapi": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz", - "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5626,6 +5643,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz", "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5665,6 +5683,7 @@ "integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.14.0", "@prisma/engines": "6.14.0" @@ -5700,7 +5719,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5758,6 +5776,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5767,6 +5786,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6198,6 +6218,15 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6486,6 +6515,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6506,6 +6536,18 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -6635,6 +6677,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6723,6 +6766,28 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 8faa323..bbbde2b 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "dependencies": { "@auth/prisma-adapter": "^2.10.0", "@prisma/client": "^6.14.0", + "mongodb": "^7.0.0", "next": "15.4.6", - "next-auth": "^4.24.11", + "next-auth": "^4.24.13", "prisma": "^6.14.0", "react": "19.1.0", "react-dom": "19.1.0" @@ -20,6 +21,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/mongodb": "^4.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/api/projects/[projectId]/images/route.ts b/src/app/api/projects/[projectId]/images/route.ts index e1cdc1d..c3b0c94 100644 --- a/src/app/api/projects/[projectId]/images/route.ts +++ b/src/app/api/projects/[projectId]/images/route.ts @@ -1,19 +1,160 @@ // Template routes: Images collection within a project (list, create) +import { NextResponse } from 'next/server'; export async function GET(_request: Request, context: { params: { projectId: string } }): Promise { - // TODO: Authenticate and authorize access to the project - // TODO: Parse query params (pagination, filters) - // TODO: Load images for the given projectId from MongoDB - // TODO: Return paginated list of images - return new Response(null); + try { + // TODO: Authenticate and authorize access to the project + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + const projectId = context.params.projectId; + + // TODO: Parse query params (pagination, filters) + const { searchParams } = new URL(_request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '20'); + const type = searchParams.get('type') || ''; // Filter by image type + const sortBy = searchParams.get('sortBy') || 'createdAt'; + const sortOrder = searchParams.get('sortOrder') || 'desc'; + + const skip = (page - 1) * limit; + + // TODO: Load images for the given projectId from MongoDB + // Mock images data - replace with actual MongoDB query + const mockImages = [ + { + id: 'img1', + projectId: projectId, + name: 'generated_image_001.png', + type: 'synthetic', + url: '/images/project1/img1.png', + metadata: { + width: 256, + height: 256, + format: 'png', + size: 102400, + labels: ['cat', 'animal'] + }, + status: 'processed', + createdAt: new Date().toISOString(), + createdBy: 'user1' + }, + { + id: 'img2', + projectId: projectId, + name: 'uploaded_photo_002.jpg', + type: 'real', + url: '/images/project1/img2.jpg', + metadata: { + width: 512, + height: 512, + format: 'jpg', + size: 204800, + labels: ['dog', 'animal'] + }, + status: 'processed', + createdAt: new Date().toISOString(), + createdBy: 'user1' + } + ]; + + // Mock pagination + const totalImages = 2; // Replace with actual count from MongoDB + const totalPages = Math.ceil(totalImages / limit); + + // TODO: Return paginated list of images + return NextResponse.json({ + images: mockImages, + pagination: { + page, + limit, + totalImages, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Error fetching project images:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function POST(request: Request, context: { params: { projectId: string } }): Promise { - // TODO: Authenticate and authorize write access to the project - // TODO: Validate request body (image metadata, storage location) - // TODO: Create image under projectId in MongoDB - // TODO: Return created image - return new Response(null); -} + try { + // TODO: Authenticate and authorize write access to the project + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + const projectId = context.params.projectId; + + // TODO: Validate request body (image metadata, storage location) + const imageData = await request.json(); + + const requiredFields = ['name', 'type', 'url']; + const missingFields = requiredFields.filter(field => !imageData[field]); + + if (missingFields.length > 0) { + return NextResponse.json( + { error: `Missing required fields: ${missingFields.join(', ')}` }, + { status: 400 } + ); + } + + // Validate image type + const allowedTypes = ['synthetic', 'real', 'augmented']; + if (!allowedTypes.includes(imageData.type)) { + return NextResponse.json( + { error: `Invalid image type. Allowed: ${allowedTypes.join(', ')}` }, + { status: 400 } + ); + } + + // Validate metadata structure + if (imageData.metadata) { + const requiredMetadata = ['width', 'height', 'format', 'size']; + const missingMetadata = requiredMetadata.filter(field => !imageData.metadata[field]); + if (missingMetadata.length > 0) { + return NextResponse.json( + { error: `Missing required metadata: ${missingMetadata.join(', ')}` }, + { status: 400 } + ); + } + } + // TODO: Create image under projectId in MongoDB + // Mock image creation - replace with actual MongoDB insert + const newImage = { + id: 'img3', // Replace with actual MongoDB generated ID + projectId: projectId, + name: imageData.name, + type: imageData.type, + url: imageData.url, + metadata: imageData.metadata || { + width: 256, + height: 256, + format: 'png', + size: 102400 + }, + status: imageData.status || 'uploaded', + createdAt: new Date().toISOString(), + createdBy: 'current-user-id' // Replace with session.user.id + }; + // TODO: Return created image + return NextResponse.json(newImage, { status: 201 }); + } catch (error) { + console.error('Error creating project image:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/projects/[projectId]/route.ts b/src/app/api/projects/[projectId]/route.ts index c8c749a..42251a8 100644 --- a/src/app/api/projects/[projectId]/route.ts +++ b/src/app/api/projects/[projectId]/route.ts @@ -1,11 +1,58 @@ // Template routes: Single project resource (read, update, delete) +import { NextResponse } from 'next/server'; export async function GET(_request: Request, context: { params: { projectId: string } }): Promise { // TODO: Authenticate user // TODO: Authorize access to project // TODO: Load project by context.params.projectId from MongoDB // TODO: Return project - return new Response(null); + + try { + // TODO: Authenticate user + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + const projectId = context.params.projectId; + + // TODO: Authorize access to project + // Check if user owns project or has access permissions + + // TODO: Load project by context.params.projectId from MongoDB + // Mock project data - replace with actual MongoDB query + const mockProject = { + id: projectId, + name: 'Sample Data Project', + description: 'A project for generating synthetic customer data', + ownerId: 'user1', + ownerName: 'Test User', + settings: { + dataType: 'tabular', + schema: { + fields: ['id', 'name', 'email', 'age'] + }, + generationConfig: { + count: 1000, + format: 'csv' + } + }, + status: 'active', + recordCount: 850, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + lastGenerated: new Date().toISOString() + }; + + // TODO: Return project + return NextResponse.json(mockProject); + } catch (error) { + console.error('Error fetching project:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function PATCH(request: Request, context: { params: { projectId: string } }): Promise { @@ -13,14 +60,113 @@ export async function PATCH(request: Request, context: { params: { projectId: st // TODO: Validate updatable fields // TODO: Update project in MongoDB // TODO: Return updated project - return new Response(null); + + try { + // TODO: Authenticate and authorize (owner or permitted role) + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + const projectId = context.params.projectId; + + // TODO: Validate updatable fields + const updates = await request.json(); + const allowedUpdates = ['name', 'description', 'settings', 'status']; + const updateKeys = Object.keys(updates); + const isValidOperation = updateKeys.every(key => allowedUpdates.includes(key)); + + if (!isValidOperation) { + return NextResponse.json( + { error: 'Invalid updates' }, + { status: 400 } + ); + } + + // Validate settings structure if provided + if (updates.settings) { + const requiredSettings = ['dataType', 'schema']; + const missingSettings = requiredSettings.filter(setting => !updates.settings[setting]); + if (missingSettings.length > 0) { + return NextResponse.json( + { error: `Missing required settings: ${missingSettings.join(', ')}` }, + { status: 400 } + ); + } + } + + // TODO: Update project in MongoDB + // Mock update - replace with actual MongoDB update + const mockUpdatedProject = { + id: projectId, + name: updates.name || 'Sample Data Project', + description: updates.description || 'A project for generating synthetic customer data', + ownerId: 'user1', + ownerName: 'Test User', + settings: updates.settings || { + dataType: 'tabular', + schema: { + fields: ['id', 'name', 'email', 'age'] + }, + generationConfig: { + count: 1000, + format: 'csv' + } + }, + status: updates.status || 'active', + recordCount: 850, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + lastGenerated: new Date().toISOString() + }; + + // TODO: Return updated project + return NextResponse.json(mockUpdatedProject); + } catch (error) { + console.error('Error updating project:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function DELETE(_request: Request, context: { params: { projectId: string } }): Promise { // TODO: Authenticate and authorize (owner or admin) // TODO: Delete project and cascade/handle related images // TODO: Return success response - return new Response(null); -} + + try { + // TODO: Authenticate and authorize (owner or admin) + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + const projectId = context.params.projectId; + + // TODO: Delete project and cascade/handle related images + console.log(`Deleting project ${projectId} and related data...`); + + // Mock delete operations: + // 1. Delete project record + // 2. Delete generated datasets/files + // 3. Delete any related images or assets + // 4. Clean up any generation jobs + + const deleteSuccess = true; // Replace with actual delete operation + if (!deleteSuccess) { + return NextResponse.json({ error: 'Project not found' }, { status: 404 }); + } + // TODO: Return success response + return new NextResponse(null, { status: 204 }); + } catch (error) { + console.error('Error deleting project:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 520da23..0ea640e 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -1,11 +1,79 @@ // Template routes: Projects collection (list all, create new) +import { NextResponse } from 'next/server'; export async function GET(request: Request): Promise { // TODO: Authenticate user // TODO: Parse query params (pagination, filters by owner, search) // TODO: Fetch projects from MongoDB (only accessible ones) // TODO: Return paginated projects list - return new Response(null); + try { + // TODO: Authenticate user + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + // TODO: Parse query params (pagination, filters by owner, search) + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + const search = searchParams.get('search') || ''; + const ownerId = searchParams.get('ownerId') || ''; + const sortBy = searchParams.get('sortBy') || 'createdAt'; + const sortOrder = searchParams.get('sortOrder') || 'desc'; + + const skip = (page - 1) * limit; + + // TODO: Fetch projects from MongoDB (only accessible ones) + // Mock data - replace with actual MongoDB query + const mockProjects = [ + { + id: '1', + name: 'E-commerce Dataset', + description: 'Product catalog with customer reviews', + ownerId: 'user1', + ownerName: 'Test User', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'active', + recordCount: 1500 + }, + { + id: '2', + name: 'Healthcare Analytics', + description: 'Patient records and treatment data', + ownerId: 'user2', + ownerName: 'Admin User', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'active', + recordCount: 800 + } + ]; + + // Mock pagination + const totalProjects = 2; // Replace with actual count from MongoDB + const totalPages = Math.ceil(totalProjects / limit); + + // TODO: Return paginated projects list + return NextResponse.json({ + projects: mockProjects, + pagination: { + page, + limit, + totalProjects, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Error fetching projects:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function POST(request: Request): Promise { @@ -13,7 +81,56 @@ export async function POST(request: Request): Promise { // TODO: Validate request body (project name, description, settings) // TODO: Create project in MongoDB associated with current user // TODO: Return created project - return new Response(null); -} + try { + // TODO: Authenticate user + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + // } + + // TODO: Validate request body (project name, description, settings) + const projectData = await request.json(); + + const requiredFields = ['name']; + const missingFields = requiredFields.filter(field => !projectData[field]); + + if (missingFields.length > 0) { + return NextResponse.json( + { error: `Missing required fields: ${missingFields.join(', ')}` }, + { status: 400 } + ); + } + + // Validate project name length + if (projectData.name.length < 3) { + return NextResponse.json( + { error: 'Project name must be at least 3 characters' }, + { status: 400 } + ); + } + // TODO: Create project in MongoDB associated with current user + // Mock project creation - replace with actual MongoDB insert + const newProject = { + id: '3', // Replace with actual MongoDB generated ID + name: projectData.name, + description: projectData.description || '', + ownerId: 'current-user-id', // Replace with session.user.id + ownerName: 'Current User', // Replace with session.user.name + settings: projectData.settings || {}, + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + recordCount: 0 + }; + // TODO: Return created project + return NextResponse.json(newProject, { status: 201 }); + } catch (error) { + console.error('Error creating project:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index 30b125c..d9c7dfd 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,10 +1,43 @@ // Template routes: Single user resource (read, update, delete) +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +// import { authOptions } from '../../auth/[...nextauth]/route'; + +const authOptions = {} as any; + export async function GET(_request: Request, context: { params: { userId: string } }): Promise { // TODO: Authenticate and authorize (self or admin) // TODO: Load user by context.params.userId from MongoDB // TODO: Return user data (omit sensitive fields) - return new Response(null); + try { + const session = await getServerSession(authOptions); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // TODO: Load user by context.params.userId from MongoDB + const userId = context.params.userId; + + // Mock user data - replace with actual MongoDB query + const mockUser = { + id: userId, + name: 'Test User', + email: 'test@example.com', + avatar: null, + bio: null, + createdAt: new Date().toISOString() + }; + + // TODO: Return user data (omit sensitive fields) + return NextResponse.json(mockUser); + } catch (error) { + console.error('Error fetching user:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function PATCH(request: Request, context: { params: { userId: string } }): Promise { @@ -12,14 +45,80 @@ export async function PATCH(request: Request, context: { params: { userId: strin // TODO: Validate updatable fields (e.g., name, email if allowed) // TODO: Update user in MongoDB // TODO: Return updated user data - return new Response(null); + try { + // TODO: Authenticate and authorize (self or admin) + const session = await getServerSession(authOptions); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // TODO: Validate updatable fields (e.g., name, email if allowed) + const updates = await request.json(); + const allowedUpdates = ['name', 'email', 'avatar', 'bio', 'phone']; + const updateKeys = Object.keys(updates); + const isValidOperation = updateKeys.every(key => allowedUpdates.includes(key)); + + if (!isValidOperation) { + return NextResponse.json( + { error: 'Invalid updates' }, + { status: 400 } + ); + } + + // TODO: Update user in MongoDB + const userId = context.params.userId; + + // Mock updated user - replace with actual MongoDB update + const mockUpdatedUser = { + id: userId, + name: updates.name || 'Test User', + email: updates.email || 'test@example.com', + avatar: updates.avatar || null, + bio: updates.bio || null, + updatedAt: new Date().toISOString() + }; + + // TODO: Return updated user data + return NextResponse.json(mockUpdatedUser); + } catch (error) { + console.error('Error updating user:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function DELETE(_request: Request, context: { params: { userId: string } }): Promise { // TODO: Authenticate and authorize (admin-only or self-delete policy) // TODO: Delete user and related data according to business rules // TODO: Return success response - return new Response(null); -} + try { + // TODO: Authenticate and authorize (admin-only or self-delete policy) + const session = await getServerSession(authOptions); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = context.params.userId; + + // TODO: Delete user and related data according to business rules + console.log(`Deleting user ${userId} and related data...`); + + // Mock delete - replace with actual MongoDB delete + const deleteSuccess = true; // Replace with actual delete operation + if (!deleteSuccess) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + // TODO: Return success response + return new NextResponse(null, { status: 204 }); + } catch (error) { + console.error('Error deleting user:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 01daa0e..e76f4c4 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -1,21 +1,140 @@ // Template routes: Users collection (list and create) // GET: List users with pagination and optional filters // POST: Create a new user (admin use-case) +import { NextResponse } from 'next/server'; export async function GET(request: Request): Promise { - // TODO: Authenticate and authorize (admin-only) + // TODO: Authenticate and authorize (admin-only) // TODO: Parse query params for pagination/sorting/filtering // TODO: Query MongoDB for users // TODO: Return paginated list of users - return new Response(null); + try { + // TODO: Authenticate and authorize (admin-only) + // const session = await getServerSession(authOptions); + // if (!session || session.user.role !== 'admin') { + // return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + // } + + // TODO: Parse query params for pagination/sorting/filtering + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + const search = searchParams.get('search') || ''; + const sortBy = searchParams.get('sortBy') || 'createdAt'; + const sortOrder = searchParams.get('sortOrder') || 'desc'; + + const skip = (page - 1) * limit; + + // TODO: Query MongoDB for users + // Mock data - replace with actual MongoDB query + const mockUsers = [ + { + id: '1', + name: 'Admin User', + email: 'admin@example.com', + role: 'admin', + createdAt: new Date().toISOString(), + avatar: null + }, + { + id: '2', + name: 'Test User', + email: 'test@example.com', + role: 'user', + createdAt: new Date().toISOString(), + avatar: null + } + ]; + + // Mock pagination + const totalUsers = 2; // Replace with actual count from MongoDB + const totalPages = Math.ceil(totalUsers / limit); + + // TODO: Return paginated list of users + return NextResponse.json({ + users: mockUsers, + pagination: { + page, + limit, + totalUsers, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Error fetching users:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } } export async function POST(request: Request): Promise { - // TODO: Authenticate and authorize (admin-only) + // TODO: Authenticate and authorize (admin-only) // TODO: Validate request body for new user // TODO: Hash password securely and create user in MongoDB // TODO: Return created user metadata - return new Response(null); -} + try { + // TODO: Authenticate and authorize (admin-only) + // const session = await getServerSession(authOptions); + // if (!session || session.user.role !== 'admin') { + // return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + // } + + // TODO: Validate request body for new user + const userData = await request.json(); + + const requiredFields = ['name', 'email', 'password']; + const missingFields = requiredFields.filter(field => !userData[field]); + + if (missingFields.length > 0) { + return NextResponse.json( + { error: `Missing required fields: ${missingFields.join(', ')}` }, + { status: 400 } + ); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(userData.email)) { + return NextResponse.json( + { error: 'Invalid email format' }, + { status: 400 } + ); + } + + // Validate password strength + if (userData.password.length < 6) { + return NextResponse.json( + { error: 'Password must be at least 6 characters' }, + { status: 400 } + ); + } + // TODO: Hash password securely and create user in MongoDB + // const hashedPassword = await hashPassword(userData.password); + + // Mock user creation - replace with actual MongoDB insert + const newUser = { + id: '3', // Replace with actual MongoDB generated ID + name: userData.name, + email: userData.email, + role: userData.role || 'user', + avatar: userData.avatar || null, + bio: userData.bio || null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + // TODO: Return created user metadata + return NextResponse.json(newUser, { status: 201 }); + } catch (error) { + console.error('Error creating user:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file