After DAL refactor

This commit is contained in:
stephen 2026-01-16 16:22:00 +11:00
parent 2f712dba23
commit e15373c169
22 changed files with 4904 additions and 1019 deletions

File diff suppressed because it is too large Load diff

Binary file not shown.

599
package-lock.json generated
View file

@ -22,7 +22,8 @@
"next-auth": "^5.0.0-beta.30", "next-auth": "^5.0.0-beta.30",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3",
"server-only": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@ -32,7 +33,7 @@
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.1", "eslint-config-next": "16.1.1",
"prisma": "^7.2.0", "prisma": "^6.19.2",
"typescript": "^5" "typescript": "^5"
} }
}, },
@ -345,73 +346,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@chevrotain/cst-dts-gen": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
"integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "10.5.0",
"@chevrotain/types": "10.5.0",
"lodash": "4.17.21"
}
},
"node_modules/@chevrotain/gast": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz",
"integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "10.5.0",
"lodash": "4.17.21"
}
},
"node_modules/@chevrotain/types": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz",
"integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz",
"integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@electric-sql/pglite": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz",
"integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@electric-sql/pglite-socket": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz",
"integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"pglite-server": "dist/scripts/server.js"
},
"peerDependencies": {
"@electric-sql/pglite": "0.3.2"
}
},
"node_modules/@electric-sql/pglite-tools": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz",
"integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==",
"devOptional": true,
"license": "Apache-2.0",
"peerDependencies": {
"@electric-sql/pglite": "0.3.2"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.0.tgz",
@ -741,19 +675,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@hono/node-server": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
"integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1318,20 +1239,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mrleebo/prisma-ast": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz",
"integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chevrotain": "^10.5.0",
"lilconfig": "^2.1.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "7.3.7", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz",
@ -1960,9 +1867,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/config": { "node_modules/@prisma/config": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz",
"integrity": "sha512-qmvSnfQ6l/srBW1S7RZGfjTQhc44Yl3ldvU6y3pgmuLM+83SBDs6UQVgMtQuMRe9J3gGqB0RF8wER6RlXEr6jQ==", "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -1978,32 +1885,6 @@
"integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/dev": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.17.0.tgz",
"integrity": "sha512-6sGebe5jxX+FEsQTpjHLzvOGPn6ypFQprcs3jcuIWv1Xp/5v6P/rjfdvAwTkP2iF6pDx2tCd8vGLNWcsWzImTA==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"@electric-sql/pglite": "0.3.2",
"@electric-sql/pglite-socket": "0.0.6",
"@electric-sql/pglite-tools": "0.2.7",
"@hono/node-server": "1.19.6",
"@mrleebo/prisma-ast": "0.12.1",
"@prisma/get-platform": "6.8.2",
"@prisma/query-plan-executor": "6.18.0",
"foreground-child": "3.3.1",
"get-port-please": "3.1.2",
"hono": "4.10.6",
"http-status-codes": "2.3.0",
"pathe": "2.0.3",
"proper-lockfile": "4.1.2",
"remeda": "2.21.3",
"std-env": "3.9.0",
"valibot": "1.2.0",
"zeptomatch": "2.0.2"
}
},
"node_modules/@prisma/driver-adapter-utils": { "node_modules/@prisma/driver-adapter-utils": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz",
@ -2014,94 +1895,69 @@
} }
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz",
"integrity": "sha512-HUeOI/SvCDsHrR9QZn24cxxZcujOjcS3w1oW/XVhnSATAli5SRMOfp/WkG3TtT5rCxDA4xOnlJkW7xkho4nURA==", "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.2.0", "@prisma/debug": "6.19.2",
"@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/fetch-engine": "7.2.0", "@prisma/fetch-engine": "6.19.2",
"@prisma/get-platform": "7.2.0" "@prisma/get-platform": "6.19.2"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
"integrity": "sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA==", "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "node_modules/@prisma/engines/node_modules/@prisma/debug": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
"integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0"
"dependencies": {
"@prisma/debug": "7.2.0"
}
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz",
"integrity": "sha512-Z5XZztJ8Ap+wovpjPD2lQKnB8nWFGNouCrglaNFjxIWAGWz0oeHXwUJRiclIoSSXN/ptcs9/behptSk8d0Yy6w==", "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.2.0", "@prisma/debug": "6.19.2",
"@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/get-platform": "7.2.0" "@prisma/get-platform": "6.19.2"
} }
}, },
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { "node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
"integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0"
"dependencies": {
"@prisma/debug": "7.2.0"
}
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "6.8.2", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz",
"integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.8.2" "@prisma/debug": "6.19.2"
} }
}, },
"node_modules/@prisma/get-platform/node_modules/@prisma/debug": { "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
"version": "6.8.2", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
"integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/query-plan-executor": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz",
"integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/studio-core": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.9.0.tgz",
"integrity": "sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g==",
"devOptional": true,
"license": "Apache-2.0",
"peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -3023,16 +2879,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axe-core": { "node_modules/axe-core": {
"version": "4.11.0", "version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
@ -3280,21 +3126,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chevrotain": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz",
"integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "10.5.0",
"@chevrotain/gast": "10.5.0",
"@chevrotain/types": "10.5.0",
"@chevrotain/utils": "10.5.0",
"lodash": "4.17.21",
"regexp-to-ast": "0.5.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -3407,7 +3238,7 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@ -3562,16 +3393,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/destr": { "node_modules/destr": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
@ -4529,23 +4350,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -4586,16 +4390,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/generator-function": { "node_modules/generator-function": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@ -4641,13 +4435,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-port-please": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz",
"integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/get-proto": { "node_modules/get-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@ -4767,20 +4554,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"devOptional": true,
"license": "ISC"
},
"node_modules/grammex": {
"version": "3.1.12",
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/has-bigints": { "node_modules/has-bigints": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@ -4900,40 +4673,6 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/hono": {
"version": "4.10.6",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz",
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/http-status-codes": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -5260,13 +4999,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"devOptional": true,
"license": "MIT"
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@ -5423,7 +5155,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"devOptional": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
@ -5594,16 +5326,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -5626,13 +5348,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -5640,13 +5355,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -5669,22 +5377,6 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lru.min": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz",
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
"devOptional": true,
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -5748,40 +5440,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mysql2": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -6187,7 +5845,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -6389,20 +6047,6 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postgres": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
"integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
"devOptional": true,
"license": "Unlicense",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/postgres-array": { "node_modules/postgres-array": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
@ -6472,34 +6116,26 @@
} }
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "7.2.0", "version": "6.19.2",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.2.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
"integrity": "sha512-jSdHWgWOgFF24+nRyyNRVBIgGDQEsMEF8KPHvhBBg3jWyR9fUAK0Nq9ThUmiGlNgq2FA7vSk/ZoCvefod+a8qg==", "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/config": "7.2.0", "@prisma/config": "6.19.2",
"@prisma/dev": "0.17.0", "@prisma/engines": "6.19.2"
"@prisma/engines": "7.2.0",
"@prisma/studio-core": "0.9.0",
"mysql2": "3.15.3",
"postgres": "3.4.7"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"
}, },
"engines": { "engines": {
"node": "^20.19 || ^22.12 || >=24.0" "node": ">=18.18"
}, },
"peerDependencies": { "peerDependencies": {
"better-sqlite3": ">=9.0.0", "typescript": ">=5.1.0"
"typescript": ">=5.4.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"better-sqlite3": {
"optional": true
},
"typescript": { "typescript": {
"optional": true "optional": true
} }
@ -6516,25 +6152,6 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proper-lockfile": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"retry": "^0.12.0",
"signal-exit": "^3.0.2"
}
},
"node_modules/proper-lockfile/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"devOptional": true,
"license": "ISC"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -6674,13 +6291,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regexp-to-ast": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
"integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -6702,16 +6312,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/remeda": {
"version": "2.21.3",
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz",
"integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"type-fest": "^4.39.1"
}
},
"node_modules/reselect": { "node_modules/reselect": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@ -6757,16 +6357,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
} }
}, },
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@ -6857,13 +6447,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@ -6880,11 +6463,11 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/seq-queue": { "node_modules/server-only": {
"version": "0.0.5", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"devOptional": true "license": "MIT"
}, },
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
@ -6997,7 +6580,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
@ -7010,7 +6593,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -7092,19 +6675,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -7132,16 +6702,6 @@
"node": ">= 10.x" "node": ">= 10.x"
} }
}, },
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stable-hash": { "node_modules/stable-hash": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@ -7149,13 +6709,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/std-env": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -7489,19 +7042,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"devOptional": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@ -7729,26 +7269,11 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/valibot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"typescript": ">=5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"devOptional": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
@ -7897,16 +7422,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/zeptomatch": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz",
"integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"grammex": "^3.1.10"
}
},
"node_modules/zod": { "node_modules/zod": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz",

View file

@ -26,7 +26,8 @@
"next-auth": "^5.0.0-beta.30", "next-auth": "^5.0.0-beta.30",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3",
"server-only": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@ -36,7 +37,7 @@
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.1", "eslint-config-next": "16.1.1",
"prisma": "^7.2.0", "prisma": "^6.19.2",
"typescript": "^5" "typescript": "^5"
} }
} }

View file

@ -7,6 +7,7 @@ export default defineConfig({
schema: "prisma/schema.prisma", schema: "prisma/schema.prisma",
datasource: { datasource: {
// This is now the ONLY place where the DB connection string is defined // This is now the ONLY place where the DB connection string is defined
url: process.env.DATABASE_URL, // prisma.config.ts
url: process.env.DATABASE_URL!,
}, },
}); });

View file

@ -1,66 +1,57 @@
// src/app/api/download/route.ts // src/app/api/download/route.ts
"use server"
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { auth } from "@/auth"; // Import the auth function from your central config import { auth } from "@/auth";
import { prisma } from '@/lib/prisma'; import { getFileNodeById } from "@/data-access/file-nodes";
import { getOneDriveContentStream } from "@/services/onedrive";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// 1. Check Authentication using the V5 auth() helper // 1. Authenticate the user session
const session = await auth(); const session = await auth();
if (!session?.user?.id) {
// In V5, tokens are usually handled in the session callback return new NextResponse("Unauthorized", { status: 401 });
if (!session || !session.accessToken) {
return new NextResponse("Unauthorized - No Access Token found", { status: 401 });
} }
// 2. Get parameters from URL // 2. Extract parameters from URL
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fileNodeId = searchParams.get('id'); const fileId = searchParams.get('id');
const mode = searchParams.get('mode') === 'inline' ? 'inline' : 'attachment'; const mode = searchParams.get('mode') === 'inline' ? 'inline' : 'attachment';
if (!fileNodeId) { if (!fileId) {
return new NextResponse("File ID is required", { status: 400 }); return new NextResponse("File ID is required", { status: 400 });
} }
// 3. Find the file in your Postgres FileNode table // 3. DAL: Fetch file metadata from local database
const fileNode = await prisma.fileNode.findUnique({ const fileNode = await getFileNodeById(fileId);
where: { id: fileNodeId }
});
if (!fileNode || !fileNode.oneDriveId) { if (!fileNode || !fileNode.oneDriveId) {
return new NextResponse("File not found in database", { status: 404 }); return new NextResponse("File not found", { status: 404 });
} }
// 4. Fetch the file stream from Microsoft Graph // 4. SERVICE: Get the binary stream from Microsoft Graph
const graphResponse = await fetch( // The service layer automatically handles the 'getFreshAccessToken' logic
`https://graph.microsoft.com/v1.0/me/drive/items/${fileNode.oneDriveId}/content`, const graphResponse = await getOneDriveContentStream(session.user.id, fileNode.oneDriveId);
{
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
);
if (!graphResponse.ok) {
const errorText = await graphResponse.text();
console.error('MS Graph Error:', errorText);
return new NextResponse(`OneDrive Error: ${graphResponse.statusText}`, { status: graphResponse.status });
}
// 5. Stream the response directly to the client // 5. Stream the response directly to the client
return new NextResponse(graphResponse.body, { // We pass the graphResponse.body (ReadableStream) directly to NextResponse
// src/app/api/download/route.ts
const metadata = fileNode.metadata as any;
const contentType = metadata?.mimeType || 'application/octet-stream';
return new NextResponse(graphResponse.body, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': fileNode.mimeType || 'application/octet-stream', 'Content-Type': contentType,
// Note: Using encodeURIComponent for filename to handle special characters
'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`, 'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`,
}, },
}); });
} catch (error) { } catch (error: any) {
console.error('Download error:', error); console.error('Download Route Error:', error);
return new NextResponse("Internal Server Error", { status: 500 }); return new NextResponse(
JSON.stringify({ error: "Internal Server Error", message: error.message }),
{ status: 500 }
);
} }
} }

View file

@ -1,138 +1,82 @@
// src/app/dashboard/actions.ts
'use server'; 'use server';
import { auth } from "@/auth"; import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { getFreshAccessToken } from "@/lib/auth-utils"; import {
getAllFileNodes,
getFileNodeById,
updateFileNode,
deleteFileNode
} from "@/data-access/file-nodes";
import {
getOneDriveItem,
deleteFromOneDrive,
uploadToOneDrive
} from "@/services/onedrive";
/** /**
* 1. FETCH: Get all file nodes for the Dashboard * 1. FETCH: Get all file nodes
* Now simply calls the DAL. Error handling is left to the caller (the UI).
*/ */
export async function getFileNodes() { export async function getFileNodes() {
try { return await getAllFileNodes();
const nodes = await prisma.fileNode.findMany({
orderBy: {
updatedAt: 'desc',
},
});
return nodes;
} catch (error) {
console.error("Error fetching file nodes:", error);
return [];
}
} }
/** /**
* 2. DOWNLOAD: Generates the authenticated OneDrive URL * 2. DOWNLOAD: Generates the authenticated OneDrive URL
* Orchestrates the session check, DAL lookup, and Service call.
*/ */
export async function getDownloadUrlAction(id: string) { export async function getDownloadUrlAction(id: string) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
const file = await prisma.fileNode.findUnique({ where: { id } }); const file = await getFileNodeById(id);
if (!file || !file.oneDriveId) throw new Error("File not found or missing cloud ID"); if (!file || !file.oneDriveId) throw new Error("File not found or missing cloud ID");
const accessToken = await getFreshAccessToken(session.user.id); // Service handles token refresh and graph request internally
const res = await fetch( const data = await getOneDriveItem(session.user.id, file.oneDriveId);
`https://graph.microsoft.com/v1.0/me/drive/items/${file.oneDriveId}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) throw new Error("Failed to contact OneDrive");
const data = await res.json();
const downloadUrl = data["@microsoft.graph.downloadUrl"]; const downloadUrl = data["@microsoft.graph.downloadUrl"];
if (!downloadUrl) throw new Error("OneDrive did not provide a download link"); if (!downloadUrl) throw new Error("OneDrive did not provide a download URL");
return downloadUrl;
return { downloadUrl };
} }
/** /**
* 3. DELETE: Remove from OneDrive (via ID) and Database * 3. DELETE: Removes from both Cloud and Database
* Folders are virtual (DB only), so cloud deletion is skipped if oneDriveId is null.
*/ */
export async function deleteFileAction(fileId: string) { export async function deleteFileNodeAction(id: string) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
const node = await prisma.fileNode.findUnique({
where: { id: fileId },
});
if (!node) {
revalidatePath("/dashboard");
return { success: true };
}
// @ts-ignore
const isAdmin = session.user.role === "ADMIN";
const isOwner = node.ownerId === session.user.id;
if (!isAdmin && !isOwner) {
throw new Error("Permission Denied.");
}
try { try {
const accessToken = await getFreshAccessToken(session.user.id); const file = await getFileNodeById(id);
if (!file) throw new Error("File record not found");
// Only attempt cloud deletion if it's a file/storage with a oneDriveId. // Phase 1: Cloud Deletion
// Virtual folders created in the DB have no oneDriveId and are skipped. if (file.oneDriveId) {
if (accessToken && node.oneDriveId) { await deleteFromOneDrive(session.user.id, file.oneDriveId);
const onedriveRes = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${accessToken}` },
}
);
if (!onedriveRes.ok && onedriveRes.status !== 404) {
console.warn("OneDrive Deletion Warning: Cloud record might still exist.");
}
}
} catch (cloudError) {
console.error("Cloud cleanup failed:", cloudError);
} }
try { // Phase 2: Database Deletion
await prisma.fileNode.delete({ where: { id: fileId } }); await deleteFileNode(id);
revalidatePath("/dashboard");
revalidatePath("/upload");
return { success: true };
} catch (dbError) {
throw new Error("Failed to remove the record from the database.");
}
}
/**
* 4. MOVE: Assign file to folder or folder to another folder (Virtual Move)
*/
export async function moveNodeAction(nodeId: string, newParentId: string | null) {
const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized");
if (nodeId === newParentId) throw new Error("Cannot move to self.");
try {
await prisma.fileNode.update({
where: { id: nodeId },
data: { parentId: newParentId }
});
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
throw new Error("Move failed."); console.error("Delete Error:", error);
return { success: false, error: "Failed to delete file" };
} }
} }
/** /**
* 5. UPDATE & REPLACE: Full update of metadata and OneDrive content * 4. UPDATE: Modify record and optionally sync new content to OneDrive
*/ */
export async function updateFileFullAction(formData: FormData) { export async function updateFileNodeAction(id: string, formData: FormData) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
const id = formData.get("id") as string;
const name = formData.get("name") as string; const name = formData.get("name") as string;
const description = formData.get("description") as string; const description = formData.get("description") as string;
const parentIdRaw = formData.get("parentId") as string; const parentIdRaw = formData.get("parentId") as string;
@ -143,44 +87,29 @@ export async function updateFileFullAction(formData: FormData) {
let metadata = JSON.parse(metadataStr); let metadata = JSON.parse(metadataStr);
try { try {
const accessToken = await getFreshAccessToken(session.user.id); const node = await getFileNodeById(id);
const node = await prisma.fileNode.findUnique({ where: { id } });
// Update physical file content only if a new file is uploaded and we have a target oneDriveId // If a new file is uploaded, push it to OneDrive first
if (newFile && newFile.size > 0 && node?.oneDriveId) { if (newFile && newFile.size > 0 && node?.oneDriveId) {
const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`; await uploadToOneDrive(session.user.id, newFile, node.oneDriveId);
const uploadRes = await fetch(onedrivePath, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": newFile.type
},
body: Buffer.from(await newFile.arrayBuffer()),
});
if (!uploadRes.ok) throw new Error("OneDrive content update failed");
metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN';
metadata.mimeType = newFile.type; metadata.mimeType = newFile.type;
} }
await prisma.fileNode.update({ // Update the database via DAL
where: { id }, await updateFileNode(id, {
data: {
name, name,
description, description,
parentId, parentId,
metadata, metadata,
size: newFile ? BigInt(newFile.size) : undefined, size: newFile ? BigInt(newFile.size) : undefined,
updatedAt: new Date(),
}
}); });
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true }; return { success: true };
} catch (error: any) { } catch (error) {
console.error("Full Update Failure:", error); console.error("Update Error:", error);
throw new Error(error.message || "Failed to update record."); return { success: false, error: "Failed to update record" };
} }
} }

View file

@ -1,7 +1,5 @@
'use client'; 'use client';
// src/app/dashboard/dashboard-view.tsx
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
@ -13,15 +11,15 @@ import {
Stack, Stack,
TextField, TextField,
InputAdornment, InputAdornment,
Tooltip Tooltip,
} from "@mui/material"; } from "@mui/material";
import { import {
DataGrid, DataGrid,
GridColDef, GridColDef,
Toolbar, GridToolbarContainer,
// Using the new non-deprecated QuickFilter components
QuickFilter, QuickFilter,
QuickFilterControl, QuickFilterControl,
QuickFilterClear,
} from "@mui/x-data-grid"; } from "@mui/x-data-grid";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
@ -30,29 +28,31 @@ import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import CancelIcon from '@mui/icons-material/Cancel';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { syncOneDrive } from "./sync-actions"; import { syncOneDrive } from "./sync-actions";
import { deleteFileAction } from "./actions"; import { deleteFileNodeAction } from "./actions";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
/**
* UPDATED TOOLBAR: Uses the new QuickFilter structure to avoid deprecation
*/
function CustomToolbar() { function CustomToolbar() {
return ( return (
<Toolbar sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <GridToolbarContainer sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6" fontWeight="bold" color="primary"> <Typography variant="h6" fontWeight="bold" color="primary">
Library Library
</Typography> </Typography>
<QuickFilter sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<QuickFilter>
<QuickFilterControl <QuickFilterControl
render={({ ref, ...controlProps }, state) => ( render={(props) => (
<TextField <TextField
{...controlProps} {...props}
inputRef={ref}
variant="outlined" variant="outlined"
size="small" size="small"
placeholder="Search files and metadata..." placeholder="Search files..."
sx={{ width: 350 }} sx={{ width: 350 }}
slotProps={{ slotProps={{
input: { input: {
@ -61,20 +61,14 @@ function CustomToolbar() {
<SearchIcon fontSize="small" /> <SearchIcon fontSize="small" />
</InputAdornment> </InputAdornment>
), ),
endAdornment: state.value ? (
<InputAdornment position="end">
<QuickFilterClear size="small">
<CancelIcon fontSize="small" />
</QuickFilterClear>
</InputAdornment>
) : null,
}, },
}} }}
/> />
)} )}
/> />
</QuickFilter> </QuickFilter>
</Toolbar> </Box>
</GridToolbarContainer>
); );
} }
@ -121,21 +115,18 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
const handleDelete = async (id: string, name: string) => { const handleDelete = async (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete "${name}"?`)) return; if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
try { try {
await deleteFileAction(id); await deleteFileNodeAction(id);
router.refresh(); router.refresh();
} catch (error: any) { } catch (error: any) {
alert(error.message || "Failed to delete file"); alert(error.message || "Failed to delete file");
} }
}; };
// --- NEW DOWNLOAD FUNCTIONS ---
const handleDownload = (id: string) => { const handleDownload = (id: string) => {
// Triggers local folder download via Content-Disposition: attachment
window.location.href = `/api/download?id=${id}&mode=attachment`; window.location.href = `/api/download?id=${id}&mode=attachment`;
}; };
const handleViewInTab = (id: string) => { const handleViewInTab = (id: string) => {
// Opens in a new tab via Content-Disposition: inline
window.open(`/api/download?id=${id}&mode=inline`, '_blank'); window.open(`/api/download?id=${id}&mode=inline`, '_blank');
}; };
@ -146,7 +137,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
flex: 1.5, flex: 1.5,
minWidth: 250, minWidth: 250,
renderCell: (params) => ( renderCell: (params) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: '100%' }}>
{params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />} {params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />}
<Typography variant="body2">{params.value}</Typography> <Typography variant="body2">{params.value}</Typography>
</Box> </Box>
@ -179,14 +170,14 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "Actions",
width: 180, // Increased width to accommodate new buttons width: 180,
align: 'right', align: 'right',
renderCell: (params) => { renderCell: (params) => {
const isOwner = params.row.ownerId === user?.id; const isOwner = params.row.ownerId === user?.id;
const isFolder = params.row.isFolder; const isFolder = params.row.isFolder;
return ( return (
<Stack direction="row" spacing={0.5} justifyContent="flex-end"> <Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
{!isFolder && ( {!isFolder && (
<> <>
<Tooltip title="View in Tab"> <Tooltip title="View in Tab">
@ -227,28 +218,25 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
</Stack> </Stack>
); );
} }
},
{
field: "metadata_search",
headerName: "Metadata Search",
width: 0,
valueGetter: (value, row) => {
if (!row.metadata) return "";
return Object.entries(row.metadata)
.filter(([k]) => k !== 'type' && k !== 'mimeType')
.map(([k, v]) => `${k}:${v}`)
.join(" ");
}
} }
]; ];
return ( return (
<Box className="space-y-4"> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button variant="outlined" startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />} onClick={handleRefresh}> <Button
variant="outlined"
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={handleRefresh}
>
Refresh List Refresh List
</Button> </Button>
<Button variant="contained" startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />} onClick={handleSync} disabled={loading}> <Button
variant="contained"
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
onClick={handleSync}
disabled={loading}
>
Sync OneDrive Sync OneDrive
</Button> </Button>
</Box> </Box>
@ -258,19 +246,11 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
rows={initialFiles} rows={initialFiles}
columns={columns} columns={columns}
slots={{ toolbar: CustomToolbar }} slots={{ toolbar: CustomToolbar }}
showToolbar
disableRowSelectionOnClick disableRowSelectionOnClick
initialState={{
columns: {
columnVisibilityModel: {
metadata_search: false,
},
},
}}
sx={{ sx={{
border: 'none', border: 'none',
'& .MuiDataGrid-columnHeaders': { bgcolor: '#f8f9fa' }, '& .MuiDataGrid-columnHeader': { bgcolor: '#f8f9fa' },
'& .MuiDataGrid-toolbarContainer': { borderBottom: '1px solid #eee' } '& .MuiDataGrid-footerContainer': { borderTop: '1px solid #eee' }
}} }}
/> />
</Box> </Box>

View file

@ -1,62 +1,49 @@
// src/app/dashboard/sync-actions.ts
'use server'; 'use server';
import { auth } from "@/auth"; import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { getFreshAccessToken } from "@/lib/auth-utils"; import { upsertFileNode } from "@/data-access/file-nodes";
import { getWebCalibreChildren } from "@/services/onedrive";
// src/app/dashboard/sync-actions.ts
//import { upsertFileNode } from "@/data-access/file-nodes"; // Change this from /services/onedrive
export async function syncOneDrive() { export async function syncOneDrive() {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
try { try {
const accessToken = await getFreshAccessToken(session.user.id); // 1. Call Service to get cloud data (token refresh handled inside service)
const response = await fetch("https://graph.microsoft.com/v1.0/me/drive/root:/WebCalibre:/children", { const items = await getWebCalibreChildren(session.user.id);
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) return { success: true, count: 0 };
const data = await response.json();
let syncedCount = 0; let syncedCount = 0;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
for (const item of data.value) { for (const item of items) {
const isFolder = !!item.folder; const isFolder = !!item.folder;
// Only skip if it's a folder AND it's a UUID (storage container) // Business Logic: Skip UUID storage folders
// If a user named a file with a UUID, we still want it. if (isFolder && uuidRegex.test(item.name)) continue;
if (isFolder && uuidRegex.test(item.name)) {
continue;
}
const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN');
await prisma.fileNode.upsert({ // 2. Call DAL to save to database
where: { oneDriveId: item.id }, // Primary match await upsertFileNode(item.id, {
update: {
name: item.name,
size: BigInt(item.size || 0),
isFolder: isFolder,
path: item.parentReference?.path + '/' + item.name,
updatedAt: new Date(),
},
create: {
id: crypto.randomUUID(),
oneDriveId: item.id,
name: item.name, name: item.name,
size: BigInt(item.size || 0), size: BigInt(item.size || 0),
isFolder: isFolder, isFolder: isFolder,
path: item.parentReference?.path + '/' + item.name, path: item.parentReference?.path + '/' + item.name,
ownerId: session.user.id, ownerId: session.user.id,
metadata: { type: extension, mimeType: item.file?.mimeType || null }, metadata: { type: extension, mimeType: item.file?.mimeType || null },
}
}); });
syncedCount++; syncedCount++;
} }
revalidatePath('/dashboard'); revalidatePath('/dashboard');
return { success: true, count: syncedCount }; return { success: true, count: syncedCount };
} catch (error: any) { } catch (error: any) {
throw new Error(error.message); console.error("Sync Error:", error.message);
throw new Error("Failed to sync with OneDrive");
} }
} }

View file

@ -1,9 +1,9 @@
'use server'; 'use server';
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getFreshAccessToken } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { createFileNode } from "@/data-access/file-nodes";
import { ensureOneDriveFolder, uploadLargeFile } from "@/services/onedrive";
export async function uploadFileAction(formData: FormData) { export async function uploadFileAction(formData: FormData) {
const session = await auth(); const session = await auth();
@ -11,58 +11,32 @@ export async function uploadFileAction(formData: FormData) {
const file = formData.get("file") as File; const file = formData.get("file") as File;
const folderName = "WebCalibre"; const folderName = "WebCalibre";
const accessToken = await getFreshAccessToken(session.user.id);
// 1. Create/Check WebCalibre Folder try {
const folderPath = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}`; // 1. Logic: Ensure destination exists
const folderCheck = await fetch(folderPath, { await ensureOneDriveFolder(session.user.id, folderName);
headers: { Authorization: `Bearer ${accessToken}` }
});
if (folderCheck.status === 404) { // 2. Logic: Perform the cloud upload
await fetch(`https://graph.microsoft.com/v1.0/me/drive/root/children`, { const driveItem = await uploadLargeFile(session.user.id, file, folderName);
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ name: folderName, folder: {} })
});
}
// 2. Create Upload Session (Supports files > 4MB) // 3. Logic: Save the result to our DB
const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}/${file.name}:/createUploadSession`; await createFileNode({
const sessionRes = await fetch(sessionUrl, {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } })
});
const { uploadUrl } = await sessionRes.json();
// 3. Upload File Data
const buffer = Buffer.from(await file.arrayBuffer());
const uploadRes = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Length": `${file.size}`,
"Content-Range": `bytes 0-${file.size - 1}/${file.size}`
},
body: buffer
});
const driveItem = await uploadRes.json();
// 4. Record in PostgreSQL
await prisma.fileNode.create({
data: {
oneDriveId: driveItem.id, oneDriveId: driveItem.id,
name: file.name, name: file.name,
size: BigInt(file.size), size: BigInt(file.size),
isFolder: false, isFolder: false,
path: `/${folderName}/${file.name}`, path: `/${folderName}/${file.name}`,
ownerId: session.user.id, ownerId: session.user.id,
metadata: { type: file.name.split('.').pop()?.toUpperCase() } metadata: {
type: file.name.split('.').pop()?.toUpperCase(),
mimeType: file.type
} }
}); });
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true }; return { success: true };
} catch (error: any) {
console.error("Upload Action Error:", error);
return { success: false, error: error.message || "Upload failed" };
}
} }

View file

@ -3,7 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { Button, Typography, Box, LinearProgress } from "@mui/material"; import { Button, Typography, Box, LinearProgress } from "@mui/material";
import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { uploadFileToOneDrive } from "./upload-actions"; import { uploadFileAction } from "./upload-actions";
export default function UploadForm() { export default function UploadForm() {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -14,7 +14,7 @@ export default function UploadForm() {
setUploading(true); setUploading(true);
try { try {
await uploadFileToOneDrive(formData); await uploadFileAction(formData);
alert("Uploaded successfully!"); alert("Uploaded successfully!");
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View file

@ -1,15 +1,13 @@
'use server'; 'use server';
// src/app/settings/actions.ts
import { auth } from "@/auth"; import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { getUserByEmail, getUserById, updateUserRole } from "@/data-access/users";
/** /**
* Toggles a user's role between 'ADMIN' and 'USER'. * Toggles a user's role between 'ADMIN' and 'USER' using the DAL pattern.
* * Security Logic:
* 1. Checks if the caller is the Bootstrap Admin (via .env).
* 2. Checks if the caller has the 'ADMIN' role in the database.
* 3. Prevents the Bootstrap Admin from being demoted to 'USER'.
*/ */
export async function toggleUserRoleAction(targetUserId: string) { export async function toggleUserRoleAction(targetUserId: string) {
const session = await auth(); const session = await auth();
@ -19,46 +17,34 @@ export async function toggleUserRoleAction(targetUserId: string) {
throw new Error("Unauthorized: No session found."); throw new Error("Unauthorized: No session found.");
} }
// 1. Authorization: Who is trying to change the role? // 1. Authorization: Verify caller's permissions via DAL
const isBootstrap = callerEmail === process.env.INITIAL_ADMIN_EMAIL; const isBootstrap = callerEmail === process.env.INITIAL_ADMIN_EMAIL;
const callerDbRecord = await getUserByEmail(callerEmail);
const callerDbRecord = await prisma.user.findUnique({
where: { email: callerEmail },
select: { role: true }
});
const isAdmin = isBootstrap || callerDbRecord?.role === "ADMIN"; const isAdmin = isBootstrap || callerDbRecord?.role === "ADMIN";
if (!isAdmin) { if (!isAdmin) {
throw new Error("Forbidden: You do not have permission to manage roles."); throw new Error("Forbidden: You do not have permission to manage roles.");
} }
// 2. Fetch the target user to be modified // 2. Fetch target user via DAL
const targetUser = await prisma.user.findUnique({ const targetUser = await getUserById(targetUserId);
where: { id: targetUserId },
select: { id: true, email: true, role: true }
});
if (!targetUser) { if (!targetUser) {
throw new Error("User not found."); throw new Error("User not found.");
} }
// 3. Protection: Prevent demoting the primary bootstrap admin // 3. Protection: Prevent demoting the primary bootstrap admin
// This ensures you don't accidentally lock yourself out of the settings page.
if (targetUser.email === process.env.INITIAL_ADMIN_EMAIL && targetUser.role === "ADMIN") { if (targetUser.email === process.env.INITIAL_ADMIN_EMAIL && targetUser.role === "ADMIN") {
throw new Error("Security Restriction: The primary Bootstrap Admin role cannot be removed."); throw new Error("Security Restriction: The primary Bootstrap Admin role cannot be removed.");
} }
// 4. Determine new role // 4. Logic: Determine new role
const newRole = targetUser.role === "ADMIN" ? "USER" : "ADMIN"; const newRole = targetUser.role === "ADMIN" ? "USER" : "ADMIN";
// 5. Execute Update // 5. Execute Update via DAL
await prisma.user.update({ await updateUserRole(targetUserId, newRole);
where: { id: targetUserId },
data: { role: newRole }
});
// 6. Refresh the data on the Settings page // 6. UI Invalidation
revalidatePath("/settings"); revalidatePath("/settings");
return { return {

View file

@ -1,15 +1,23 @@
'use server'; 'use server';
//src/app/update/[id]/_actions.ts // src/app/update/[id]/_actions.ts
import { auth } from "@/auth"; import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes";
/**
* SERVER ACTION: Updates file metadata and organizational data.
* This refactored version uses the Data Access Layer (DAL) to
* ensure separation of concerns.
*/
export async function updateFileAction(formData: FormData) { export async function updateFileAction(formData: FormData) {
const session = await auth(); const session = await auth();
// 1. Authorization Guard
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
// 2. Data Extraction
const id = formData.get("id") as string; const id = formData.get("id") as string;
const name = formData.get("name") as string; const name = formData.get("name") as string;
const description = formData.get("description") as string; const description = formData.get("description") as string;
@ -20,30 +28,39 @@ export async function updateFileAction(formData: FormData) {
const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {};
try { try {
// 1. Get existing record to preserve system metadata (like mimeType) // 3. DAL: Fetch existing record to safely merge metadata
const existing = await prisma.fileNode.findUnique({ where: { id } }); // This replaces the direct prisma.fileNode.findUnique call
const existingMetadata = (existing?.metadata as Record<string, any>) || {}; const existing = await getFileNodeById(id);
if (!existing) throw new Error("File record not found");
// 2. Update the record const existingMetadata = (existing.metadata as Record<string, any>) || {};
await prisma.fileNode.update({
where: { id }, // 4. Logic: Prepare the updated metadata object
data: { const updatedMetadata = {
...customMetadata, // Apply new user keys
type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE",
mimeType: existingMetadata.mimeType // Ensure system metadata isn't overwritten
};
// 5. DAL: Perform the update
// This replaces the direct prisma.fileNode.update call
await updateFileNode(id, {
name, name,
description, description,
parentId, parentId,
metadata: { metadata: updatedMetadata,
...customMetadata, // User's new keys
type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE",
mimeType: existingMetadata.mimeType // Preserve the original mimeType
}
}
}); });
// 6. Cache Invalidation
revalidatePath("/dashboard"); revalidatePath("/dashboard");
revalidatePath(`/update/${id}`); revalidatePath(`/update/${id}`);
return { success: true }; return { success: true };
} catch (error: any) { } catch (error: any) {
console.error("Update error:", error); console.error("Update action error:", error);
return { success: false, message: error.message }; return {
success: false,
message: error.message || "An unexpected error occurred during update"
};
} }
} }

View file

@ -116,7 +116,7 @@ export default function UpdateView({ fileNode, folders }: any) {
<Stack spacing={2}> <Stack spacing={2}>
{customMetadata.map((row, index) => ( {customMetadata.map((row, index) => (
<Grid container spacing={1} key={index} alignItems="center"> <Grid container spacing={1} key={index} alignItems="center">
<Grid item xs={5}> <Grid size={ {xs:5}}>
<TextField <TextField
fullWidth size="small" placeholder="Key" fullWidth size="small" placeholder="Key"
value={row.key} value={row.key}
@ -127,7 +127,7 @@ export default function UpdateView({ fileNode, folders }: any) {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={6}> <Grid size={{xs:6}}>
<TextField <TextField
fullWidth size="small" placeholder="Value" fullWidth size="small" placeholder="Value"
value={row.value} value={row.value}
@ -138,7 +138,7 @@ export default function UpdateView({ fileNode, folders }: any) {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={1}> <Grid size={{xs:1}}>
<IconButton color="error" onClick={() => setCustomMetadata(customMetadata.filter((_, i) => i !== index))}> <IconButton color="error" onClick={() => setCustomMetadata(customMetadata.filter((_, i) => i !== index))}>
<DeleteOutlineIcon /> <DeleteOutlineIcon />
</IconButton> </IconButton>

View file

@ -1,14 +1,16 @@
'use server'; 'use server';
// src/app/upload/_actions.ts
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getFreshAccessToken } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { createFileNode } from "@/data-access/file-nodes";
//import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive";
import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive";
/** /**
* 1. CREATE FOLDER: Virtual Only * 1. CREATE VIRTUAL FOLDER
* Logic: User-created organizational folders exist ONLY in the database.
* No call to OneDrive is made here.
*/ */
export async function createFolderAction(name: string, parentId?: string | null) { export async function createFolderAction(name: string, parentId?: string | null) {
const session = await auth(); const session = await auth();
@ -16,34 +18,26 @@ export async function createFolderAction(name: string, parentId?: string | null)
try { try {
const internalId = crypto.randomUUID(); const internalId = crypto.randomUUID();
// Swapped createNode for createFileNode
const newNode = await prisma.fileNode.create({ const newNode = await createFileNode({
data: {
id: internalId, id: internalId,
oneDriveId: null, // Virtual folders do not have a cloud ID oneDriveId: null,
name: name, name,
isFolder: true, isFolder: true,
path: `virtual:/${name}`, path: `virtual:/${name}`,
ownerId: session.user.id, ownerId: session.user.id,
parentId: parentId || null, parentId: parentId || null,
metadata: { type: "FOLDER" } metadata: { type: "FOLDER" }
}
}); });
revalidatePath("/upload");
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true, node: newNode }; return { success: true, node: newNode };
} catch (error: any) { } catch (error: any) {
console.error("Folder creation error:", error);
throw new Error(error.message || "Failed to create virtual folder"); throw new Error(error.message || "Failed to create virtual folder");
} }
} }
/** /**
* 2. UPLOAD FILE: Physical Container * 2. UPLOAD FILE (Physical UUID Folder)
* Logic: Creates a physical folder (UUID) on OneDrive to hold the file.
* This ensures every file has a unique storage space in the cloud.
*/ */
export async function uploadFileAction(formData: FormData) { export async function uploadFileAction(formData: FormData) {
const session = await auth(); const session = await auth();
@ -59,61 +53,29 @@ export async function uploadFileAction(formData: FormData) {
if (!file) throw new Error("No file selected"); if (!file) throw new Error("No file selected");
const accessToken = await getFreshAccessToken(session.user.id);
const rootFolder = "WebCalibre"; const rootFolder = "WebCalibre";
const internalId = crypto.randomUUID(); // This UUID will be the OneDrive folder name const internalId = crypto.randomUUID(); // Used for both DB ID and OneDrive Folder Name
// 1. Create the Physical Storage Folder on OneDrive try {
const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { // A. Ensure root exists
method: "POST", await ensureOneDriveFolder(session.user.id, rootFolder);
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: internalId,
folder: {},
"@microsoft.graph.conflictBehavior": "fail"
})
});
if (!createSubFolderRes.ok) { // B. Create the physical UUID folder on OneDrive
const errorData = await createSubFolderRes.json(); const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId);
throw new Error(errorData.error?.message || "Storage directory creation failed"); const subFolderData = await subFolderRes.json();
}
const subFolderData = await createSubFolderRes.json();
// 2. Create Upload Session inside the new Physical Folder // C. Upload the file binary into that specific folder
const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`; const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id);
const sessionRes = await fetch(sessionUrl, {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } })
});
const { uploadUrl } = await sessionRes.json(); // D. Create record in Database
const buffer = Buffer.from(await file.arrayBuffer()); // src/app/upload/_actions.ts
// 3. PUT the file binary // ... inside uploadFileAction or createFolderAction ...
const uploadRes = await fetch(uploadUrl, { // ... inside uploadFileAction after OneDrive work is done ...
method: "PUT",
headers: {
"Content-Length": `${file.size}`,
"Content-Range": `bytes 0-${file.size - 1}/${file.size}`
},
body: buffer
});
const uploadedFileData = await uploadRes.json(); await createFileNode({
const oneDriveId = uploadedFileData.id;
const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN";
// 4. Create record in Database
// Link it to the VIRTUAL folder via parentId
await prisma.fileNode.create({
data: {
id: internalId, id: internalId,
oneDriveId: oneDriveId, oneDriveId: uploadedFileData.id,
name: file.name, name: file.name,
description: description, description: description,
size: BigInt(file.size), size: BigInt(file.size),
@ -122,14 +84,17 @@ export async function uploadFileAction(formData: FormData) {
ownerId: session.user.id, ownerId: session.user.id,
parentId: parentId, parentId: parentId,
metadata: { metadata: {
...customMetadata, ...customMetadata, // User's custom keys from the form
type: extension, type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN",
mimeType: file.type mimeType: file.type
} }
} });
});
revalidatePath("/dashboard"); revalidatePath("/dashboard");
revalidatePath("/upload"); revalidatePath("/upload");
return { success: true }; return { success: true };
} catch (error: any) {
console.error("Upload refactor error:", error);
return { success: false, error: error.message };
}
} }

View file

@ -208,19 +208,19 @@ export default function UploadView({ user, folders = [] }: any) {
<Stack spacing={2}> <Stack spacing={2}>
{customMetadata.map((row, index) => ( {customMetadata.map((row, index) => (
<Grid container spacing={1} key={index} alignItems="center"> <Grid container spacing={1} key={index} alignItems="center">
<Grid item xs={5}> <Grid size={{xs:5}}>
<TextField <TextField
fullWidth size="small" placeholder="Key (e.g. Project-ID)" fullWidth size="small" placeholder="Key (e.g. Project-ID)"
value={row.key} onChange={(e) => updateMetadataRow(index, 'key', e.target.value)} value={row.key} onChange={(e) => updateMetadataRow(index, 'key', e.target.value)}
/> />
</Grid> </Grid>
<Grid item xs={6}> <Grid size= {{xs:6}}>
<TextField <TextField
fullWidth size="small" placeholder="Value" fullWidth size="small" placeholder="Value"
value={row.value} onChange={(e) => updateMetadataRow(index, 'value', e.target.value)} value={row.value} onChange={(e) => updateMetadataRow(index, 'value', e.target.value)}
/> />
</Grid> </Grid>
<Grid item xs={1}> <Grid size= {{xs:1}}>
<IconButton color="error" onClick={() => removeMetadataRow(index)}> <IconButton color="error" onClick={() => removeMetadataRow(index)}>
<DeleteOutlineIcon /> <DeleteOutlineIcon />
</IconButton> </IconButton>

View file

@ -0,0 +1,18 @@
'use client';
import { signOut } from "next-auth/react";
import { Button } from "@mui/material";
import LogoutIcon from '@mui/icons-material/Logout';
export default function LogoutButton() {
return (
<Button
variant="outlined"
color="error"
startIcon={<LogoutIcon />}
onClick={() => signOut({ callbackUrl: "/" })}
>
Sign Out
</Button>
);
}

View file

@ -1,12 +1,19 @@
'use client'; 'use client';
import React from "react";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
// ... other imports like ThemeProvider import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from '@/lib/theme'; // Import your custom theme here
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<SessionProvider> <SessionProvider>
<AppRouterCacheProvider> <AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
{/* CssBaseline resets browser styles to match MUI and your theme */}
<CssBaseline />
{children} {children}
</ThemeProvider> </ThemeProvider>
</AppRouterCacheProvider> </AppRouterCacheProvider>

View file

@ -0,0 +1,104 @@
// src/data-access/file-nodes.ts
import "server-only";
import { prisma } from "@/lib/prisma";
/**
* FETCH: Retrieve all nodes for the dashboard.
* Centralizing this here allows us to change sort order or filters
* in one place for the entire application.
*/
export async function getAllFileNodes() {
return await prisma.fileNode.findMany({
orderBy: {
updatedAt: 'desc',
},
});
}
/**
* FETCH: Get a single node by ID.
* Used by the Download route and Update pages to verify a file exists.
*/
export async function getFileNodeById(id: string) {
return await prisma.fileNode.findUnique({
where: { id },
});
}
/**
* UPDATE: Modify metadata, name, or virtual location.
* This function accepts the data object to keep the DAL flexible.
*/
export async function updateFileNode(id: string, data: any) {
return await prisma.fileNode.update({
where: { id },
data: {
...data,
updatedAt: new Date(),
},
});
}
/**
* DELETE: Remove the record from the database.
* Cloud deletion should be handled by the Service Layer before calling this.
*/
export async function deleteFileNode(id: string) {
return await prisma.fileNode.delete({
where: { id },
});
}
/**
* MASTER CREATE: Handles both standard uploads and virtual folders.
* If no ID is provided, it generates a fresh UUID.
*/
export async function createFileNode(data: {
id?: string; // Optional: used for virtual folders/UUID storage
oneDriveId: string | null;
name: string;
description?: string;
isFolder: boolean;
path: string;
ownerId: string;
parentId?: string | null; // Optional: for nested structures
size?: bigint;
metadata: any;
}) {
return await prisma.fileNode.create({
data: {
...data,
id: data.id ?? crypto.randomUUID(), // Use provided ID or generate new one
}
});
}
// ... other functions (getAllFileNodes, etc)
/**
* UPSERT: Create or Update a file node based on OneDrive ID
* Moved here because it interacts with the Database.
*/
export async function upsertFileNode(oneDriveId: string, data: any) {
return await prisma.fileNode.upsert({
where: { oneDriveId },
update: {
name: data.name,
size: data.size,
isFolder: data.isFolder,
path: data.path,
updatedAt: new Date(),
},
create: {
id: crypto.randomUUID(),
oneDriveId: oneDriveId,
name: data.name,
size: data.size,
isFolder: data.isFolder,
path: data.path,
ownerId: data.ownerId,
metadata: data.metadata,
}
});
}

42
src/data-access/users.ts Normal file
View file

@ -0,0 +1,42 @@
import "server-only";
import { prisma } from "@/lib/prisma";
/**
* FETCH: Get user by Email
* Used for authorization checks in actions.
*/
export async function getUserByEmail(email: string) {
return await prisma.user.findUnique({
where: { email },
select: { id: true, email: true, role: true }
});
}
/**
* FETCH: Get user by ID
*/
export async function getUserById(id: string) {
return await prisma.user.findUnique({
where: { id },
select: { id: true, email: true, role: true }
});
}
/**
* UPDATE: Update user role
*/
export async function updateUserRole(id: string, role: "ADMIN" | "USER") {
return await prisma.user.update({
where: { id },
data: { role }
});
}
/**
* FETCH: List all users (for the settings table)
*/
export async function getAllUsers() {
return await prisma.user.findMany({
orderBy: { email: 'asc' }
});
}

View file

@ -1,7 +1,6 @@
import { createTheme } from '@mui/material/styles'; import { createTheme } from '@mui/material/styles';
import { Roboto } from 'next/font/google'; import { Roboto } from 'next/font/google';
// Load the font optimized for Next.js
const roboto = Roboto({ const roboto = Roboto({
weight: ['300', '400', '500', '700'], weight: ['300', '400', '500', '700'],
subsets: ['latin'], subsets: ['latin'],
@ -9,39 +8,36 @@ const roboto = Roboto({
}); });
const theme = createTheme({ const theme = createTheme({
// 1. Color Palette (Clean & Professional for a Library App)
palette: { palette: {
mode: 'light', mode: 'light',
primary: { primary: {
main: '#1976d2', // Professional Blue main: '#1976d2',
}, },
secondary: { secondary: {
main: '#9c27b0', // Purple for accents main: '#9c27b0',
}, },
background: { background: {
default: '#f4f6f8', // Light grey for the app background default: '#f4f6f8',
paper: '#ffffff', paper: '#ffffff',
}, },
}, },
// 2. Typography
typography: { typography: {
fontFamily: roboto.style.fontFamily, fontFamily: roboto.style.fontFamily,
h6: { h6: {
fontWeight: 600, fontWeight: 600,
}, },
}, },
// 3. Component Defaults
components: { components: {
// Keep your button styles
MuiButton: { MuiButton: {
styleOverrides: { styleOverrides: {
root: { root: {
textTransform: 'none', // Prevents all-caps buttons textTransform: 'none',
borderRadius: 8, borderRadius: 8,
}, },
}, },
}, },
// Keep your paper styles
MuiPaper: { MuiPaper: {
defaultProps: { defaultProps: {
elevation: 2, elevation: 2,
@ -52,6 +48,12 @@ const theme = createTheme({
}, },
}, },
}, },
// ADD THIS: Ensures the theme is compatible with MUI v7's Grid logic
MuiStack: {
defaultProps: {
useFlexGap: true,
},
},
}, },
}); });

172
src/services/onedrive.ts Normal file
View file

@ -0,0 +1,172 @@
// src/services/onedrive.ts
import "server-only";
import { getFreshAccessToken } from "@/lib/auth-utils";
/**
* PRIVATE HELPER: graphRequest
* This internal function handles the heavy lifting of fetching tokens
* and making the actual HTTP call to Microsoft.
*/
async function graphRequest(userId: string, endpoint: string, options: RequestInit = {}) {
// 1. Automatically handle token refresh logic
const token = await getFreshAccessToken(userId);
const baseUrl = "https://graph.microsoft.com/v1.0";
const res = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
// 2. Centralized Error Handling for OneDrive
if (!res.ok) {
const errorData = await res.text();
console.error(`OneDrive API Error [${endpoint}]:`, errorData);
throw new Error(`OneDrive API failed: ${res.statusText}`);
}
return res;
}
/**
* SERVICE: Download File Content
* Returns the raw binary stream from OneDrive.
*/
export async function getOneDriveContentStream(userId: string, oneDriveId: string) {
return await graphRequest(userId, `/me/drive/items/${oneDriveId}/content`);
}
/**
* SERVICE: Get File Metadata
* Used to get the @microsoft.graph.downloadUrl or driveItem properties.
*/
export async function getOneDriveItem(userId: string, oneDriveId: string) {
const res = await graphRequest(userId, `/me/drive/items/${oneDriveId}`);
return res.json();
}
/**
* SERVICE: Upload File
* Handles the PUT request to OneDrive for new or updated files.
*/
export async function uploadToOneDrive(userId: string, file: File, oneDriveId?: string) {
// If oneDriveId exists, we update. Otherwise, we'd use a path (needs expansion for new files).
const endpoint = oneDriveId
? `/me/drive/items/${oneDriveId}/content`
: `/me/drive/root:/${file.name}:/content`;
return await graphRequest(userId, endpoint, {
method: "PUT",
headers: { "Content-Type": file.type },
body: Buffer.from(await file.arrayBuffer()),
});
}
/**
* SERVICE: Delete from Cloud
*/
export async function deleteFromOneDrive(userId: string, oneDriveId: string) {
return await graphRequest(userId, `/me/drive/items/${oneDriveId}`, {
method: "DELETE",
});
}
/**
* SERVICE: List Children of the WebCalibre folder
*/
export async function getWebCalibreChildren(userId: string) {
const res = await graphRequest(userId, "/me/drive/root:/WebCalibre:/children");
const data = await res.json();
return data.value; // Returns the array of driveItems
}
/**
* SERVICE: Ensure a specific folder exists in OneDrive
* Returns the folder ID
*/
export async function ensureOneDriveFolder(userId: string, folderName: string) {
try {
const res = await graphRequest(userId, `/me/drive/root:/${folderName}`);
const data = await res.json();
return data.id;
} catch (error) {
// If 404, create it
const createRes = await graphRequest(userId, `/me/drive/root/children`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: folderName, folder: {} })
});
const data = await createRes.json();
return data.id;
}
}
/**
* SERVICE: Upload Large File via Session
* This replaces the basic PUT for better reliability
*/
export async function uploadLargeFile(userId: string, file: File, folderName: string) {
// 1. Create Upload Session
const sessionRes = await graphRequest(userId, `/me/drive/root:/${folderName}/${file.name}:/createUploadSession`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } })
});
const { uploadUrl } = await sessionRes.json();
// 2. Upload the data to the provided URL (No Authorization header needed for the uploadUrl itself)
const buffer = Buffer.from(await file.arrayBuffer());
const uploadRes = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Length": `${file.size}`,
"Content-Range": `bytes 0-${file.size - 1}/${file.size}`
},
body: buffer
});
if (!uploadRes.ok) throw new Error("Upload session failed");
return await uploadRes.json(); // Returns the DriveItem
}
/**
* SERVICE: Create a folder by name inside a parent path
*/
export async function createOneDriveFolder(userId: string, parentPath: string, folderName: string) {
return await graphRequest(userId, `/me/drive/root:/${parentPath}:/children`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: folderName,
folder: {},
"@microsoft.graph.conflictBehavior": "fail"
})
});
}
/**
* SERVICE: Upload to a specific folder ID (using session)
*/
export async function uploadToFolderId(userId: string, file: File, folderId: string) {
const sessionRes = await graphRequest(userId, `/me/drive/items/${folderId}:/${encodeURIComponent(file.name)}:/createUploadSession`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } })
});
const { uploadUrl } = await sessionRes.json();
const buffer = Buffer.from(await file.arrayBuffer());
const uploadRes = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Length": `${file.size}`,
"Content-Range": `bytes 0-${file.size - 1}/${file.size}`
},
body: buffer
});
if (!uploadRes.ok) throw new Error("Upload failed");
return await uploadRes.json();
}