Compare commits

...

9 Commits

Author SHA1 Message Date
Michel Roegl-Brunner
c33e4f004e chore: commit via Cursor on 2025-10-20 2025-10-20 14:01:28 +02:00
Michel Roegl-Brunner
3161f347ca Merge pull request #187 from community-scripts/dependabot/npm_and_yarn/eslint-9.38.0 2025-10-18 15:27:45 +02:00
dependabot[bot]
a6ce06a230 build(deps-dev): Bump eslint from 9.37.0 to 9.38.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.37.0 to 9.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.37.0...v9.38.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 22:51:33 +00:00
dependabot[bot]
fe6245c264 build(deps-dev): Bump prettier-plugin-tailwindcss from 0.7.0 to 0.7.1 (#190) 2025-10-18 00:50:29 +02:00
Michel Roegl-Brunner
d6e50cafda Merge pull request #189 from community-scripts/dependabot/npm_and_yarn/next-15.5.6 2025-10-18 00:50:15 +02:00
dependabot[bot]
59c67452fc build(deps-dev): Bump @types/node from 24.8.0 to 24.8.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.8.0 to 24.8.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-18 00:50:01 +02:00
dependabot[bot]
dfc572cb59 build(deps-dev): Bump eslint-config-next from 15.5.5 to 15.5.6
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.5.5 to 15.5.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.5.6/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-18 00:49:35 +02:00
dependabot[bot]
828f99819a build(deps): Bump next from 15.5.5 to 15.5.6
Bumps [next](https://github.com/vercel/next.js) from 15.5.5 to 15.5.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.5...v15.5.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 19:30:23 +00:00
github-actions[bot]
f8404d2695 chore: add VERSION v0.4.5 (#183)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-17 13:27:57 +00:00
16 changed files with 418 additions and 137 deletions

View File

@@ -1 +1 @@
0.4.4
0.4.5

153
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.546.0",
"next": "^15.5.5",
"next": "^15.5.6",
"node-pty": "^1.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -51,18 +51,18 @@
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.8.0",
"@types/node": "^24.8.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"eslint": "^9.23.0",
"eslint-config-next": "^15.5.5",
"eslint": "^9.38.0",
"eslint-config-next": "^15.5.6",
"jsdom": "^27.0.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.0",
"prettier-plugin-tailwindcss": "^0.7.1",
"prisma": "^6.17.1",
"tailwindcss": "^4.1.14",
"typescript": "^5.8.2",
@@ -1116,13 +1116,13 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.6",
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -1131,9 +1131,9 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1181,9 +1181,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.37.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
"version": "9.38.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1194,9 +1194,9 @@
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1840,15 +1840,15 @@
}
},
"node_modules/@next/env": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz",
"integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz",
"integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.5.tgz",
"integrity": "sha512-FMzm412l9oFB8zdRD+K6HQ1VzlS+sNNsdg0MfvTg0i8lfCyTgP/RFxiu/pGJqZ/IQnzn9xSiLkjOVI7Iv4nbdQ==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz",
"integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1856,9 +1856,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz",
"integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz",
"integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==",
"cpu": [
"arm64"
],
@@ -1872,9 +1872,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz",
"integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz",
"integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==",
"cpu": [
"x64"
],
@@ -1888,9 +1888,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz",
"integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz",
"integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==",
"cpu": [
"arm64"
],
@@ -1904,9 +1904,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz",
"integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz",
"integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==",
"cpu": [
"arm64"
],
@@ -1920,9 +1920,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz",
"integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz",
"integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==",
"cpu": [
"x64"
],
@@ -1936,9 +1936,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz",
"integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz",
"integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==",
"cpu": [
"x64"
],
@@ -1952,9 +1952,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz",
"integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz",
"integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==",
"cpu": [
"arm64"
],
@@ -1968,9 +1968,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz",
"integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz",
"integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==",
"cpu": [
"x64"
],
@@ -3749,9 +3749,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz",
"integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz",
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
@@ -6080,25 +6080,24 @@
}
},
"node_modules/eslint": {
"version": "9.37.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"version": "9.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.4.0",
"@eslint/config-array": "^0.21.1",
"@eslint/config-helpers": "^0.4.1",
"@eslint/core": "^0.16.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.37.0",
"@eslint/js": "9.38.0",
"@eslint/plugin-kit": "^0.4.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
@@ -6141,13 +6140,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.5.tgz",
"integrity": "sha512-f8lRSSelp6cqrYjxEMjJ5En3WV913gTu/w9goYShnIujwDSQlKt4x9MwSDiduE9R5mmFETK44+qlQDxeSA0rUA==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz",
"integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "15.5.5",
"@next/eslint-plugin-next": "15.5.6",
"@rushstack/eslint-patch": "^1.10.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
@@ -9634,12 +9633,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz",
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==",
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz",
"integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.5",
"@next/env": "15.5.6",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -9652,14 +9651,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.5",
"@next/swc-darwin-x64": "15.5.5",
"@next/swc-linux-arm64-gnu": "15.5.5",
"@next/swc-linux-arm64-musl": "15.5.5",
"@next/swc-linux-x64-gnu": "15.5.5",
"@next/swc-linux-x64-musl": "15.5.5",
"@next/swc-win32-arm64-msvc": "15.5.5",
"@next/swc-win32-x64-msvc": "15.5.5",
"@next/swc-darwin-arm64": "15.5.6",
"@next/swc-darwin-x64": "15.5.6",
"@next/swc-linux-arm64-gnu": "15.5.6",
"@next/swc-linux-arm64-musl": "15.5.6",
"@next/swc-linux-x64-gnu": "15.5.6",
"@next/swc-linux-x64-musl": "15.5.6",
"@next/swc-win32-arm64-msvc": "15.5.6",
"@next/swc-win32-x64-msvc": "15.5.6",
"sharp": "^0.34.3"
},
"peerDependencies": {
@@ -10205,9 +10204,9 @@
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.0.tgz",
"integrity": "sha512-zpRZhkfwq1cNmbKhmKzXKuKFdkgXZXlf6p+KttD75v6pGz1FxmcKMc4RKdw97GYBKBbout4113HSLaBJAomFDw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.1.tgz",
"integrity": "sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -41,7 +41,7 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.546.0",
"next": "^15.5.5",
"next": "^15.5.6",
"node-pty": "^1.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -65,18 +65,18 @@
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.8.0",
"@types/node": "^24.8.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"eslint": "^9.23.0",
"eslint-config-next": "^15.5.5",
"eslint": "^9.38.0",
"eslint-config-next": "^15.5.6",
"jsdom": "^27.0.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.0",
"prettier-plugin-tailwindcss": "^0.7.1",
"prisma": "^6.17.1",
"tailwindcss": "^4.1.14",
"typescript": "^5.8.2",

View File

@@ -8,12 +8,15 @@ import stripAnsi from 'strip-ansi';
import { spawn as ptySpawn } from 'node-pty';
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
import { getDatabase } from './src/server/database-prisma.js';
import { registerGlobalErrorHandlers } from './src/server/logging/globalHandlers.js';
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);
const app = next({ dev, hostname, port });
// Register global handlers once at bootstrap
registerGlobalErrorHandlers();
const handle = app.getRequestHandler();
// WebSocket handler for script execution

View File

@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../../server/database-prisma';
import type { CreateServerData } from '../../../../types/server';
import { withApiLogging } from '../../../../server/logging/withApiLogging';
export async function GET(
export const GET = withApiLogging(async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
@@ -28,16 +29,16 @@ export async function GET(
}
return NextResponse.json(server);
} catch (error) {
console.error('Error fetching server:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to fetch server' },
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function PUT(
export const PUT = withApiLogging(async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
@@ -62,8 +63,9 @@ export async function PUT(
);
}
// Validate SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
// Coerce and validate SSH port
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
if (Number.isNaN(port) || port < 1 || port > 65535) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
@@ -111,7 +113,7 @@ export async function PUT(
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: port,
color,
key_generated: key_generated ?? false,
ssh_key_path
@@ -124,7 +126,7 @@ export async function PUT(
}
);
} catch (error) {
console.error('Error updating server:', error);
// Error handled by withApiLogging
// Handle unique constraint violation
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
@@ -139,9 +141,9 @@ export async function PUT(
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function DELETE(
export const DELETE = withApiLogging(async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
@@ -177,12 +179,12 @@ export async function DELETE(
changes: 1
}
);
} catch (error) {
console.error('Error deleting server:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to delete server' },
{ status: 500 }
);
}
}
}, { redactBody: true });

View File

@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getSSHService } from '../../../../server/ssh-service';
import { getDatabase } from '../../../../server/database-prisma';
import { withApiLogging } from '../../../../server/logging/withApiLogging';
export async function POST(_request: NextRequest) {
export const POST = withApiLogging(async function POST(_request: NextRequest) {
try {
const sshService = getSSHService();
const db = getDatabase();
@@ -20,7 +21,7 @@ export async function POST(_request: NextRequest) {
serverId: serverId
});
} catch (error) {
console.error('Error generating SSH key pair:', error);
// Error handled by withApiLogging
return NextResponse.json(
{
success: false,
@@ -29,4 +30,4 @@ export async function POST(_request: NextRequest) {
{ status: 500 }
);
}
}
}, { redactBody: true });

View File

@@ -2,22 +2,23 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../server/database-prisma';
import type { CreateServerData } from '../../../types/server';
import { withApiLogging } from '../../../server/logging/withApiLogging';
export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
const db = getDatabase();
const servers = await db.getAllServers();
return NextResponse.json(servers);
} catch (error) {
console.error('Error fetching servers:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to fetch servers' },
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated, ssh_key_path }: CreateServerData = body;
@@ -30,8 +31,9 @@ export async function POST(request: NextRequest) {
);
}
// Validate SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
// Coerce and validate SSH port
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
if (Number.isNaN(port) || port < 1 || port > 65535) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
@@ -69,7 +71,7 @@ export async function POST(request: NextRequest) {
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: port,
color,
key_generated: key_generated ?? false,
ssh_key_path
@@ -82,11 +84,10 @@ export async function POST(request: NextRequest) {
},
{ status: 201 }
);
} catch (error) {
console.error('Error creating server:', error);
} catch {
// Error handled by withApiLogging
// Handle unique constraint violation
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
if (Error instanceof Error && Error.message.includes('UNIQUE constraint failed')) {
return NextResponse.json(
{ error: 'A server with this name already exists' },
{ status: 409 }
@@ -98,5 +99,5 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}, { redactBody: true });

View File

@@ -3,8 +3,9 @@ import { NextResponse } from 'next/server';
import { getAuthConfig, updateAuthCredentials, updateAuthEnabled } from '~/lib/auth';
import fs from 'fs';
import path from 'path';
import { withApiLogging } from '../../../../server/logging/withApiLogging';
export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
const authConfig = getAuthConfig();
@@ -14,16 +15,16 @@ export async function GET() {
hasCredentials: authConfig.hasCredentials,
setupCompleted: authConfig.setupCompleted,
});
} catch (error) {
console.error('Error reading auth credentials:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to read auth configuration' },
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const { username, password, enabled } = await request.json() as { username: string; password: string; enabled?: boolean };
@@ -54,16 +55,16 @@ export async function POST(request: NextRequest) {
success: true,
message: 'Authentication credentials updated successfully'
});
} catch (error) {
console.error('Error updating auth credentials:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to update auth credentials' },
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function PATCH(request: NextRequest) {
export const PATCH = withApiLogging(async function PATCH(request: NextRequest) {
try {
const { enabled } = await request.json() as { enabled: boolean };
@@ -107,11 +108,11 @@ export async function PATCH(request: NextRequest) {
success: true,
message: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully`
});
} catch (error) {
console.error('Error updating auth enabled status:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to update auth status' },
{ status: 500 }
);
}
}
}, { redactBody: true });

View File

@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { withApiLogging } from '../../../../server/logging/withApiLogging';
export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const { token } = await request.json();
@@ -39,16 +40,16 @@ export async function POST(request: NextRequest) {
fs.writeFileSync(envPath, envContent);
return NextResponse.json({ success: true, message: 'GitHub token saved successfully' });
} catch (error) {
console.error('Error saving GitHub token:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to save GitHub token' },
{ status: 500 }
);
}
}
}, { redactBody: true });
export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
// Path to the .env file
const envPath = path.join(process.cwd(), '.env');
@@ -65,11 +66,11 @@ export async function GET() {
const token = githubTokenMatch ? githubTokenMatch[1] : null;
return NextResponse.json({ token });
} catch (error) {
console.error('Error reading GitHub token:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to read GitHub token' },
{ status: 500 }
);
}
}
}, { redactBody: true });

View File

@@ -4,6 +4,7 @@ import { type NextRequest } from "next/server";
import { env } from "~/env.js";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import logger from "../../../../server/logging/logger";
const handler = (req: NextRequest) =>
fetchRequestHandler({
@@ -14,9 +15,7 @@ const handler = (req: NextRequest) =>
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
logger.error("trpc_error", { path: path ?? "<no-path>" }, error);
}
: undefined,
});

View File

@@ -4,6 +4,8 @@ const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: ['warn', 'error']
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

View File

@@ -0,0 +1,21 @@
import logger from './logger';
import { toSafeError } from './prismaSafeError';
let registered = false;
export function registerGlobalErrorHandlers() {
if (registered) return;
registered = true;
process.on('uncaughtException', (err) => {
const safe = toSafeError(err);
logger.error('uncaught_exception', { name: safe.name, code: safe.code }, err);
});
process.on('unhandledRejection', (reason) => {
const safe = toSafeError(reason as any);
logger.error('unhandled_rejection', { name: safe.name, code: safe.code }, reason);
});
}

View File

@@ -0,0 +1,81 @@
import { redactObject } from './redact';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40
};
const envLevel = (process.env.LOG_LEVEL as LogLevel) || 'info';
const currentLevel = LOG_LEVELS[envLevel] ?? LOG_LEVELS.info;
function safeMeta(meta?: unknown): unknown {
if (meta === undefined) return undefined;
try {
return redactObject(meta);
} catch {
return undefined;
}
}
function safeError(err: unknown): { name?: string; code?: string; stack?: string } | undefined {
if (!err) return undefined;
if (err instanceof Error) {
return {
name: err.name,
code: (err as any).code,
stack: formatStack(err.stack)
};
}
return undefined;
}
function formatStack(stack?: string): string | undefined {
if (!stack) return undefined;
const lines = stack.split('\n').slice(0, 10);
return lines.join('\n');
}
function log(level: LogLevel, message: string, meta?: unknown, err?: unknown) {
if (LOG_LEVELS[level] < currentLevel) return;
const payload: Record<string, unknown> = {
level,
msg: message,
time: new Date().toISOString(),
};
const redactedMeta = safeMeta(meta);
if (redactedMeta !== undefined) payload.meta = redactedMeta;
const safeErr = safeError(err);
if (safeErr) payload.err = safeErr;
const line = JSON.stringify(payload);
if (level === 'error') {
console.error(line);
} else if (level === 'warn') {
console.warn(line);
} else {
console.log(line);
}
}
export const logger = {
debug(message: string, meta?: unknown) {
log('debug', message, meta);
},
info(message: string, meta?: unknown) {
log('info', message, meta);
},
warn(message: string, meta?: unknown) {
log('warn', message, meta);
},
error(message: string, meta?: unknown, err?: unknown) {
log('error', message, meta, err);
}
};
export default logger;

View File

@@ -0,0 +1,36 @@
type SafeError = { code?: string; name: string; safeMessage: string };
export function toSafeError(err: unknown): SafeError {
if (err && typeof err === 'object') {
const name = (err as any).name as string | undefined;
const code = (err as any).code as string | undefined;
// Prisma error names to map
if (name === 'PrismaClientValidationError') {
return { name, code, safeMessage: 'Invalid input' };
}
if (name === 'PrismaClientKnownRequestError') {
// Avoid echoing message which may include parameters
return { name, code, safeMessage: 'Database constraint or known request error' };
}
if (name === 'PrismaClientUnknownRequestError') {
return { name, code, safeMessage: 'Database request failed' };
}
if (name === 'PrismaClientRustPanicError') {
return { name, code, safeMessage: 'Database engine error' };
}
if (name === 'PrismaClientInitializationError') {
return { name, code, safeMessage: 'Database initialization failed' };
}
if (name === 'PrismaClientFetchEngineError') {
return { name, code, safeMessage: 'Database engine fetch error' };
}
if (name) {
return { name, code, safeMessage: 'Unhandled server error' };
}
}
return { name: 'Error', safeMessage: 'Unhandled server error' };
}

View File

@@ -0,0 +1,86 @@
const DEFAULT_MASK = '***REDACTED***';
const DEFAULT_SENSITIVE_KEYS = [
'password',
'ssh_key',
'sshKey',
'ssh_key_passphrase',
'sshKeyPassphrase',
'token',
'access_token',
'refresh_token',
'authorization',
'cookie',
'set-cookie',
'secret',
'apiKey',
'apikey'
];
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]';
}
export function redactObject(
value: unknown,
opts?: { keys?: string[]; mask?: string }
): unknown {
const keys = (opts?.keys ?? DEFAULT_SENSITIVE_KEYS).map(k => k.toLowerCase());
const mask = opts?.mask ?? DEFAULT_MASK;
const visit = (val: unknown): unknown => {
if (val === null || val === undefined) return val;
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') return val;
if (Array.isArray(val)) return val.map(visit);
if (val instanceof Map) {
const mapped = new Map();
for (const [k, v] of val.entries()) {
const shouldRedact = typeof k === 'string' && keys.includes(k.toLowerCase());
mapped.set(k, shouldRedact ? mask : visit(v));
}
return mapped;
}
if (isPlainObject(val)) {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(val)) {
const shouldRedact = keys.includes(k.toLowerCase());
out[k] = shouldRedact ? mask : visit(v);
}
return out;
}
return maskIfLikelySecret(val, mask);
};
return visit(value);
}
function maskIfLikelySecret(val: unknown, mask: string): unknown {
// For safety, non-serializable or unexpected values get masked when logged
try {
JSON.stringify(val);
return val;
} catch {
return mask;
}
}
export function summarizeBody(body: unknown): { keys: string[]; size?: number } {
try {
if (isPlainObject(body)) {
const keys = Object.keys(body);
const size = Buffer.from(JSON.stringify(body)).length;
return { keys, size };
}
if (Array.isArray(body)) {
const size = Buffer.from(JSON.stringify(body)).length;
return { keys: ['<array>'], size };
}
return { keys: [typeof body], size: undefined };
} catch {
return { keys: ['<unserializable>'] };
}
}
export const SENSITIVE_KEYS = DEFAULT_SENSITIVE_KEYS;

View File

@@ -0,0 +1,48 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import logger from './logger';
import { redactObject, summarizeBody } from './redact';
import { toSafeError } from './prismaSafeError';
type Handler = (request: NextRequest, context?: any) => Promise<Response> | Response;
export function withApiLogging(
handler: Handler,
opts?: { redactBody?: boolean }
) {
const { redactBody = false } = opts ?? {};
return async function wrapped(request: NextRequest, context?: any) {
const url = new URL(request.url);
const method = request.method;
const path = url.pathname;
const queryKeys = Array.from(url.searchParams.keys());
try {
let meta: Record<string, unknown> = { method, path, queryKeys };
if (method !== 'GET' && method !== 'HEAD') {
try {
const body = await request.clone().json();
meta = {
...meta,
body: redactBody ? undefined : redactObject(body),
bodySummary: redactBody ? summarizeBody(body) : undefined
};
} catch {
// Ignore non-JSON bodies
}
}
logger.info('api_request', meta);
const response = await handler(request, context);
logger.info('api_response', { method, path, status: response.status });
return response;
} catch (err) {
const safe = toSafeError(err);
logger.error('api_error', { method, path, code: safe.code, name: safe.name }, err);
return NextResponse.json({ error: safe.safeMessage }, { status: 500 });
}
};
}