Quellcode durchsuchen

feat: Installs feathers casl

Owen Diffey vor 5 Monaten
Ursprung
Commit
f0b3fde227

+ 253 - 2
frontend/package-lock.json

@@ -9,6 +9,8 @@
       "version": "3.11.0",
       "license": "GPL-3.0",
       "dependencies": {
+        "@casl/ability": "^6.7.2",
+        "@casl/vue": "^2.2.2",
         "@feathersjs/feathers": "^5.0.31",
         "@feathersjs/socketio-client": "^5.0.31",
         "@intlify/unplugin-vue-i18n": "^5.3.0",
@@ -18,6 +20,7 @@
         "date-fns": "^4.1.0",
         "dompurify": "^3.1.7",
         "eslint-config-airbnb-base": "^15.0.0",
+        "feathers-casl": "^2.1.2",
         "feathers-pinia": "^4.5.4",
         "marked": "^15.0.0",
         "musare-server": "http://server:3030/server-0.0.0.tgz",
@@ -139,6 +142,28 @@
       "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
       "dev": true
     },
+    "node_modules/@casl/ability": {
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz",
+      "integrity": "sha512-KjKXlcjKbUz8dKw7PY56F7qlfOFgxTU6tnlJ8YrbDyWkJMIlHa6VRWzCD8RU20zbJUC1hExhOFggZjm6tf1mUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@ucast/mongo2js": "^1.3.0"
+      },
+      "funding": {
+        "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
+      }
+    },
+    "node_modules/@casl/vue": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/@casl/vue/-/vue-2.2.2.tgz",
+      "integrity": "sha512-xWy4i5+3+WuBgENVesPalRTKpSJZ2cEMXtbqjWjqj7FDvoeso7jT1pBVk9ujKlIRhgfVWGdCRb7XzeISi2VLcA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@casl/ability": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0",
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/@colors/colors": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -2418,6 +2443,41 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@ucast/core": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
+      "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@ucast/js": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.4.tgz",
+      "integrity": "sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@ucast/core": "^1.0.0"
+      }
+    },
+    "node_modules/@ucast/mongo": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
+      "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@ucast/core": "^1.4.1"
+      }
+    },
+    "node_modules/@ucast/mongo2js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
+      "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@ucast/core": "^1.6.1",
+        "@ucast/js": "^3.0.0",
+        "@ucast/mongo": "^2.4.0"
+      }
+    },
     "node_modules/@ungap/structured-clone": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -5007,6 +5067,15 @@
       "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
       "dev": true
     },
+    "node_modules/fast-equals": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
+      "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/fast-glob": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -5057,6 +5126,47 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/feathers-casl": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/feathers-casl/-/feathers-casl-2.1.2.tgz",
+      "integrity": "sha512-TYRd31DfdedsBCva4qL2PFvEpYT5MkmU7+HyJqonOyGH7LEEQBLEn63u5wQ8A+cxXeS6v0Q66PWZ8upb6hT5rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@feathersjs/errors": "^5.0.24",
+        "@feathersjs/feathers": "^5.0.24",
+        "@feathersjs/transport-commons": "^5.0.24",
+        "feathers-hooks-common": "^8.1.2",
+        "feathers-utils": "^3.1.3",
+        "lodash": "^4.17.21"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "peerDependencies": {
+        "@casl/ability": "6.x",
+        "@feathersjs/feathers": "^5.0.0"
+      }
+    },
+    "node_modules/feathers-hooks-common": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/feathers-hooks-common/-/feathers-hooks-common-8.2.1.tgz",
+      "integrity": "sha512-t3gLAaTY5ufnAoKczLCNXJddq/L9ORrvxAlfuU2Azgoi0b6X3t0Y91l1JCJAvixzxxwLJmgdnKpFX32+CvEn0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@feathersjs/errors": "^5.0.29",
+        "ajv": "^6.12.6",
+        "debug": "^4.3.5",
+        "graphql": "^16.9.0",
+        "lodash": "^4.17.21",
+        "neotraverse": "^0.6.14"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@feathersjs/feathers": "^5.0.0"
+      }
+    },
     "node_modules/feathers-pinia": {
       "version": "4.5.4",
       "resolved": "https://registry.npmjs.org/feathers-pinia/-/feathers-pinia-4.5.4.tgz",
@@ -5123,6 +5233,26 @@
         }
       }
     },
+    "node_modules/feathers-utils": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/feathers-utils/-/feathers-utils-3.1.3.tgz",
+      "integrity": "sha512-49cEbxO+l7f1/dUXt31KuyZwzxeXMFq4AnYdsZJsZ2X5qMOvy7OqIXcAoaHoIp/HHu/TUcT0ahvN15PDP7atVA==",
+      "license": "MIT",
+      "dependencies": {
+        "@feathersjs/adapter-commons": "^5.0.11",
+        "@feathersjs/commons": "^5.0.11",
+        "@feathersjs/errors": "^5.0.11",
+        "fast-equals": "^5.0.1",
+        "feathers-hooks-common": "^8.1.1",
+        "lodash": "^4.17.21"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@feathersjs/feathers": "^5.0.0"
+      }
+    },
     "node_modules/fecha": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -5571,6 +5701,15 @@
       "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
       "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
     },
+    "node_modules/graphql": {
+      "version": "16.9.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
+      "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+      }
+    },
     "node_modules/has-bigints": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -7258,8 +7397,9 @@
       "name": "server",
       "version": "0.0.0",
       "resolved": "http://server:3030/server-0.0.0.tgz",
-      "integrity": "sha512-rT+S57DYb9hzYxcedKY1JkjWy+FOuFtGu2Kylk9DUqt8uJ+Xu38wUmoqO5wpbvZbazUKY8ex/bZAOi1J7g8fQw==",
+      "integrity": "sha512-h2tWuS4yv3rbwYfbI3YVJHh6i+9bok74IdwrVfp3CfTCl6x1q6Gp7U9jyEcTfRGaKmkKd9U/OqFH2HeiXW29KQ==",
       "dependencies": {
+        "@casl/ability": "^6.7.2",
         "@feathersjs/adapter-commons": "^5.0.31",
         "@feathersjs/authentication": "^5.0.31",
         "@feathersjs/authentication-client": "^5.0.31",
@@ -7274,6 +7414,7 @@
         "@feathersjs/socketio": "^5.0.31",
         "@feathersjs/transport-commons": "^5.0.31",
         "@feathersjs/typebox": "^5.0.31",
+        "feathers-casl": "^2.1.2",
         "knex": "^3.1.0",
         "pg": "^8.13.1",
         "winston": "^3.17.0"
@@ -7329,6 +7470,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/neotraverse": {
+      "version": "0.6.18",
+      "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
+      "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/nopt": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
@@ -10195,6 +10345,20 @@
       "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
       "dev": true
     },
+    "@casl/ability": {
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz",
+      "integrity": "sha512-KjKXlcjKbUz8dKw7PY56F7qlfOFgxTU6tnlJ8YrbDyWkJMIlHa6VRWzCD8RU20zbJUC1hExhOFggZjm6tf1mUw==",
+      "requires": {
+        "@ucast/mongo2js": "^1.3.0"
+      }
+    },
+    "@casl/vue": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/@casl/vue/-/vue-2.2.2.tgz",
+      "integrity": "sha512-xWy4i5+3+WuBgENVesPalRTKpSJZ2cEMXtbqjWjqj7FDvoeso7jT1pBVk9ujKlIRhgfVWGdCRb7XzeISi2VLcA==",
+      "requires": {}
+    },
     "@colors/colors": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -11597,6 +11761,37 @@
         "eslint-visitor-keys": "^3.4.3"
       }
     },
+    "@ucast/core": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
+      "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
+    },
+    "@ucast/js": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.4.tgz",
+      "integrity": "sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==",
+      "requires": {
+        "@ucast/core": "^1.0.0"
+      }
+    },
+    "@ucast/mongo": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
+      "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
+      "requires": {
+        "@ucast/core": "^1.4.1"
+      }
+    },
+    "@ucast/mongo2js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
+      "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
+      "requires": {
+        "@ucast/core": "^1.6.1",
+        "@ucast/js": "^3.0.0",
+        "@ucast/mongo": "^2.4.0"
+      }
+    },
     "@ungap/structured-clone": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -13480,6 +13675,11 @@
       "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
       "dev": true
     },
+    "fast-equals": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
+      "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ=="
+    },
     "fast-glob": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -13525,6 +13725,32 @@
         "reusify": "^1.0.4"
       }
     },
+    "feathers-casl": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/feathers-casl/-/feathers-casl-2.1.2.tgz",
+      "integrity": "sha512-TYRd31DfdedsBCva4qL2PFvEpYT5MkmU7+HyJqonOyGH7LEEQBLEn63u5wQ8A+cxXeS6v0Q66PWZ8upb6hT5rg==",
+      "requires": {
+        "@feathersjs/errors": "^5.0.24",
+        "@feathersjs/feathers": "^5.0.24",
+        "@feathersjs/transport-commons": "^5.0.24",
+        "feathers-hooks-common": "^8.1.2",
+        "feathers-utils": "^3.1.3",
+        "lodash": "^4.17.21"
+      }
+    },
+    "feathers-hooks-common": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/feathers-hooks-common/-/feathers-hooks-common-8.2.1.tgz",
+      "integrity": "sha512-t3gLAaTY5ufnAoKczLCNXJddq/L9ORrvxAlfuU2Azgoi0b6X3t0Y91l1JCJAvixzxxwLJmgdnKpFX32+CvEn0A==",
+      "requires": {
+        "@feathersjs/errors": "^5.0.29",
+        "ajv": "^6.12.6",
+        "debug": "^4.3.5",
+        "graphql": "^16.9.0",
+        "lodash": "^4.17.21",
+        "neotraverse": "^0.6.14"
+      }
+    },
     "feathers-pinia": {
       "version": "4.5.4",
       "resolved": "https://registry.npmjs.org/feathers-pinia/-/feathers-pinia-4.5.4.tgz",
@@ -13554,6 +13780,19 @@
         }
       }
     },
+    "feathers-utils": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/feathers-utils/-/feathers-utils-3.1.3.tgz",
+      "integrity": "sha512-49cEbxO+l7f1/dUXt31KuyZwzxeXMFq4AnYdsZJsZ2X5qMOvy7OqIXcAoaHoIp/HHu/TUcT0ahvN15PDP7atVA==",
+      "requires": {
+        "@feathersjs/adapter-commons": "^5.0.11",
+        "@feathersjs/commons": "^5.0.11",
+        "@feathersjs/errors": "^5.0.11",
+        "fast-equals": "^5.0.1",
+        "feathers-hooks-common": "^8.1.1",
+        "lodash": "^4.17.21"
+      }
+    },
     "fecha": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -13872,6 +14111,11 @@
       "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
       "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
     },
+    "graphql": {
+      "version": "16.9.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
+      "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw=="
+    },
     "has-bigints": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -15061,8 +15305,9 @@
     },
     "musare-server": {
       "version": "http://server:3030/server-0.0.0.tgz",
-      "integrity": "sha512-rT+S57DYb9hzYxcedKY1JkjWy+FOuFtGu2Kylk9DUqt8uJ+Xu38wUmoqO5wpbvZbazUKY8ex/bZAOi1J7g8fQw==",
+      "integrity": "sha512-h2tWuS4yv3rbwYfbI3YVJHh6i+9bok74IdwrVfp3CfTCl6x1q6Gp7U9jyEcTfRGaKmkKd9U/OqFH2HeiXW29KQ==",
       "requires": {
+        "@casl/ability": "^6.7.2",
         "@feathersjs/adapter-commons": "^5.0.31",
         "@feathersjs/authentication": "^5.0.31",
         "@feathersjs/authentication-client": "^5.0.31",
@@ -15077,6 +15322,7 @@
         "@feathersjs/socketio": "^5.0.31",
         "@feathersjs/transport-commons": "^5.0.31",
         "@feathersjs/typebox": "^5.0.31",
+        "feathers-casl": "^2.1.2",
         "knex": "^3.1.0",
         "pg": "^8.13.1",
         "winston": "^3.17.0"
@@ -15107,6 +15353,11 @@
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
       "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="
     },
+    "neotraverse": {
+      "version": "0.6.18",
+      "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
+      "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="
+    },
     "nopt": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",

+ 3 - 0
frontend/package.json

@@ -41,6 +41,8 @@
     "vue-tsc": "^2.1.10"
   },
   "dependencies": {
+    "@casl/ability": "^6.7.2",
+    "@casl/vue": "^2.2.2",
     "@feathersjs/feathers": "^5.0.31",
     "@feathersjs/socketio-client": "^5.0.31",
     "@intlify/unplugin-vue-i18n": "^5.3.0",
@@ -50,6 +52,7 @@
     "date-fns": "^4.1.0",
     "dompurify": "^3.1.7",
     "eslint-config-airbnb-base": "^15.0.0",
+    "feathers-casl": "^2.1.2",
     "feathers-pinia": "^4.5.4",
     "marked": "^15.0.0",
     "musare-server": "http://server:3030/server-0.0.0.tgz",

+ 4 - 1
frontend/src/components/MainHeader.vue

@@ -8,6 +8,7 @@ import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
 import { useAuthStore } from "@/stores/auth";
+import { useAbility } from "@casl/vue";
 
 const ChristmasLights = defineAsyncComponent(
 	() => import("@/components/ChristmasLights.vue")
@@ -40,6 +41,8 @@ const { nightmode } = storeToRefs(userPreferencesStore);
 
 const { openModal } = useModalsStore();
 
+const { can } = useAbility();
+
 const toggleNightmode = toggle => {
 	localNightmode.value =
 		toggle === undefined ? !localNightmode.value : toggle;
@@ -128,7 +131,7 @@ onMounted(async () => {
 			</div>
 			<span v-if="authStore.isAuthenticated" class="grouped">
 				<router-link
-					v-if="hasPermission('admin.view')"
+					v-if="can('view', 'admin')"
 					class="nav-item admin"
 					to="/admin"
 				>

+ 1 - 2
frontend/src/feathers.ts

@@ -3,7 +3,6 @@ import socketio from "@feathersjs/socketio-client"
 import io from "socket.io-client"
 import { createPiniaClient } from "feathers-pinia"
 import { pinia } from "./pinia";
-import { useAuthStore } from "./stores/auth";
 
 const socket = io(`${document.location.protocol}//${document.location.host}`, {
     path: "/api/socket.io",
@@ -14,7 +13,7 @@ const feathersClient = createClient(socketio(socket), { storage: window.localSto
 
 export const api = createPiniaClient(feathersClient, {
     pinia,
-    idField: '_id',
+    idField: 'id',
     // optional
     ssr: false,
     whitelist: [],

+ 27 - 1
frontend/src/main.ts

@@ -16,8 +16,10 @@ import i18n from "@/i18n";
 import AppComponent from "./App.vue";
 
 import { pinia } from "./pinia";
-import { useAuthStore } from "./stores/auth";
 import { api } from "./feathers";
+import { abilitiesPlugin } from "@casl/vue";
+import { useCaslStore } from "./stores/casl";
+import { useAuthStore } from "./stores/auth";
 
 const handleMetadata = attrs => {
 	const configStore = useConfigStore();
@@ -262,6 +264,30 @@ const router = createRouter({
 
 app.use(pinia);
 
+const authStore = useAuthStore();
+const caslStore = useCaslStore();
+
+app.use(abilitiesPlugin, caslStore.ability);
+
+authStore.$onAction(async ({ name, after }) => {
+	switch (name) {
+	  case 'authenticate':
+	  case 'reAuthenticate': {
+		after(result => {
+			caslStore.rules = result ? result.rules : []
+		})
+	  } break;
+	  case 'logout': 
+	  case 'isTokenExpired': {
+		after(() => caslStore.rules = [])
+	  } break;
+	  default:
+		break;
+	}
+  })
+
+authStore.reAuthenticate();
+
 // console.log(222, await api.service('users').create({
 // 	username: 'test',
 // 	email: 'test@test.com',

+ 2 - 4
frontend/src/stores/auth.ts

@@ -3,10 +3,8 @@ import { api } from '@/feathers'
 import { useAuth } from 'feathers-pinia'
 
 export const useAuthStore = defineStore('auth', () => {
-  const auth = useAuth({ api, servicePath: 'users' });
-  auth.reAuthenticate();
-  return auth;
-})
+  return useAuth({ api, servicePath: 'users' });
+});
 
 if (import.meta.hot)
   import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))

+ 32 - 0
frontend/src/stores/casl.ts

@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia'
+import { Ability, createAliasResolver, createMongoAbility } from '@casl/ability';
+import { ref, watch } from 'vue';
+
+export const useCaslStore = defineStore('casl', () => {
+  const resolveAction = createAliasResolver({
+    update: 'patch',       // define the same rules for update & patch
+    read: ['get', 'find'], // use 'read' as a equivalent for 'get' & 'find'
+    delete: 'remove'       // use 'delete' or 'remove'
+  });
+  const detectSubjectType = (subject: any) => {
+    if (typeof subject === 'string') return subject;
+    return subject.constructor.servicePath;
+  }
+
+  const ability = ref(
+    createMongoAbility([], {
+      detectSubjectType,
+      resolveAction
+    })
+  );
+  const rules = ref([]);
+
+  watch(rules, value => {
+    ability.value.update(value);
+  });
+
+  return {
+    ability,
+    rules
+  };
+});

+ 1 - 0
server/migrations/20241117173947_user.ts

@@ -5,6 +5,7 @@ export async function up(knex: Knex): Promise<void> {
   await knex.schema.createTable('users', (table) => {
     table.increments('id');
     table.string('username').unique();
+    table.enum('role', ['user', 'moderator', 'admin']).defaultTo('user');
     table.string('email').unique();
     table.string('password');
   })

+ 2 - 0
server/package.json

@@ -46,6 +46,7 @@
     "migrate:make": "knex migrate:make"
   },
   "dependencies": {
+    "@casl/ability": "^6.7.2",
     "@feathersjs/adapter-commons": "^5.0.31",
     "@feathersjs/authentication": "^5.0.31",
     "@feathersjs/authentication-client": "^5.0.31",
@@ -60,6 +61,7 @@
     "@feathersjs/socketio": "^5.0.31",
     "@feathersjs/transport-commons": "^5.0.31",
     "@feathersjs/typebox": "^5.0.31",
+    "feathers-casl": "^2.1.2",
     "knex": "^3.1.0",
     "pg": "^8.13.1",
     "winston": "^3.17.0"

+ 2 - 2
server/src/app.ts

@@ -8,9 +8,9 @@ import { configurationValidator } from './configuration'
 import type { Application } from './declarations'
 import { logError } from './hooks/log-error'
 import { postgresql } from './postgresql'
-import { authentication } from './authentication'
 import { services } from './services/index'
 import { channels } from './channels'
+import { feathersCasl } from 'feathers-casl'
 
 const app: Application = koa(feathers())
 
@@ -34,9 +34,9 @@ app.configure(
   })
 )
 app.configure(postgresql)
-app.configure(authentication)
 app.configure(services)
 app.configure(channels)
+app.configure(feathersCasl());
 
 // Register hooks that run on all service methods
 app.hooks({

+ 13 - 0
server/src/channels.ts

@@ -4,6 +4,7 @@ import type { AuthenticationResult } from '@feathersjs/authentication'
 import '@feathersjs/transport-commons'
 import type { Application, HookContext } from './declarations'
 import { logger } from './logger'
+import { getChannelsWithReadAbility, makeChannelOptions } from 'feathers-casl'
 
 export const channels = (app: Application) => {
   logger.warn(
@@ -19,6 +20,12 @@ export const channels = (app: Application) => {
     // connection can be undefined if there is no
     // real-time connection, e.g. when logging in via REST
     if (connection) {
+      // this is needed to map the ability from the authentication hook to the connection so it gets available in the HookContext as `params.ability` automatically
+      if (authResult.ability) {
+        connection.ability = authResult.ability;
+        connection.rules = authResult.rules;
+      }
+
       // The connection is no longer anonymous, remove it
       app.channel('anonymous').leave(connection)
 
@@ -35,4 +42,10 @@ export const channels = (app: Application) => {
     // e.g. to publish all service events to all authenticated users use
     return app.channel('authenticated')
   })
+
+  const caslOptions = makeChannelOptions(app);
+
+  app.publish((data: any, context: HookContext) => {
+    return getChannelsWithReadAbility(app, data, context, caslOptions);
+  });
 }

+ 40 - 0
server/src/services/authentication/authentication.abilities.ts

@@ -0,0 +1,40 @@
+import { Ability, AbilityBuilder, createAliasResolver } from "@casl/ability";
+import { User } from "../../client";
+import { Role } from "../users/users.schema";
+
+// don't forget this, as `read` is used internally
+const resolveAction = createAliasResolver({
+  update: "patch", // define the same rules for update & patch
+  read: ["get", "find"], // use 'read' as a equivalent for 'get' & 'find'
+  delete: "remove" // use 'delete' or 'remove'
+});
+
+export const defineRulesFor = (user: User) => {
+  // also see https://casl.js.org/v6/en/guide/define-rules
+  const { can, cannot, rules } = new AbilityBuilder(Ability);
+
+  if (user.role === Role.ADMIN) {
+    can("manage", "all");
+    return rules;
+  }
+
+  if (user.role === Role.MODERATOR) {
+    can("view", "admin");
+  }
+
+  can("read", "users");
+  can("update", "users", { id: user.id });
+  cannot("update", "users", ["roleId"], { id: user.id });
+  cannot("delete", "users", { id: user.id });
+
+  can("manage", "tasks", { userId: user.id });
+  can("create-multi", "posts", { userId: user.id });
+
+  return rules;
+};
+
+export const defineAbilitiesFor = (user: User) => {
+  const rules = defineRulesFor(user);
+
+  return new Ability(rules, { resolveAction });
+};

+ 42 - 0
server/src/services/authentication/authentication.hooks.ts

@@ -0,0 +1,42 @@
+import { HookContext } from "@feathersjs/feathers";
+import { defineAbilitiesFor } from "./authentication.abilities";
+
+export default {
+  before: {
+    all: [],
+    find: [],
+    get: [],
+    create: [],
+    update: [],
+    patch: [],
+    remove: []
+  },
+  after: {
+    all: [],
+    find: [],
+    get: [],
+    create: [
+      (context: HookContext) => {
+        const { user } = context.result;
+        if (!user) return context;
+        const ability = defineAbilitiesFor(user);
+        context.result.ability = ability;
+        context.result.rules = ability.rules;
+
+        return context;
+      }
+    ],
+    update: [],
+    patch: [],
+    remove: []
+  },
+  error: {
+    all: [],
+    find: [],
+    get: [],
+    create: [],
+    update: [],
+    patch: [],
+    remove: []
+  }
+};

+ 6 - 3
server/src/authentication.ts → server/src/services/authentication/authentication.ts

@@ -2,9 +2,10 @@
 import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'
 import { LocalStrategy } from '@feathersjs/authentication-local'
 
-import type { Application } from './declarations'
+import type { Application } from '../../declarations'
+import authenticationHooks from './authentication.hooks'
 
-declare module './declarations' {
+declare module '../../declarations' {
   interface ServiceTypes {
     authentication: AuthenticationService
   }
@@ -16,5 +17,7 @@ export const authentication = (app: Application) => {
   authentication.register('jwt', new JWTStrategy())
   authentication.register('local', new LocalStrategy())
 
-  app.use('authentication', authentication)
+  app.use('authentication', authentication);
+
+  app.service('authentication').hooks(authenticationHooks);
 }

+ 3 - 1
server/src/services/index.ts

@@ -1,8 +1,10 @@
 import { user } from './users/users'
 // For more information about this file see https://dove.feathersjs.com/guides/cli/application.html#configure-functions
 import type { Application } from '../declarations'
+import { authentication } from './authentication/authentication';
 
 export const services = (app: Application) => {
-  app.configure(user)
+  app.configure(authentication);
+  app.configure(user);
   // All services will be registered here
 }

+ 16 - 8
server/src/services/users/users.schema.ts

@@ -1,6 +1,6 @@
 // // For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
 import { resolve } from '@feathersjs/schema'
-import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
+import { StringEnum, Type, getValidator, querySyntax } from '@feathersjs/typebox'
 import type { Static } from '@feathersjs/typebox'
 import { passwordHash } from '@feathersjs/authentication-local'
 
@@ -9,10 +9,18 @@ import { dataValidator, queryValidator } from '../../validators'
 import type { UserService } from './users.class'
 
 // Main data model schema
+export enum Role {
+  USER = "user",
+  MODERATOR = "moderator",
+  ADMIN = "admin"
+};
 export const userSchema = Type.Object(
   {
     id: Type.Number(),
     username: Type.String(),
+    role: StringEnum(Object.values(Role), {
+      default: Role.USER
+    }),
     email: Type.String(),
     password: Type.Optional(Type.String())
   },
@@ -48,7 +56,7 @@ export const userPatchResolver = resolve<User, HookContext<UserService>>({
 })
 
 // Schema for allowed query properties
-export const userQueryProperties = Type.Pick(userSchema, ['id','username', 'email'])
+export const userQueryProperties = Type.Pick(userSchema, ['id','username', 'role', 'email'])
 export const userQuerySchema = Type.Intersect(
   [
     querySyntax(userQueryProperties),
@@ -61,11 +69,11 @@ export type UserQuery = Static<typeof userQuerySchema>
 export const userQueryValidator = getValidator(userQuerySchema, queryValidator)
 export const userQueryResolver = resolve<UserQuery, HookContext<UserService>>({
   // If there is a user (e.g. with authentication), they are only allowed to see their own data
-  id: async (value, user, context) => {
-    if (context.params.user) {
-      return context.params.user.id
-    }
+  // id: async (value, user, context) => {
+  //   if (context.params.user) {
+  //     return context.params.user.id
+  //   }
 
-    return value
-  }
+  //   return value
+  // }
 })

+ 22 - 8
server/src/services/users/users.ts

@@ -14,9 +14,11 @@ import {
   userQueryResolver
 } from './users.schema'
 
-import type { Application } from '../../declarations'
+import type { Application, HookContext } from '../../declarations'
 import { UserService, getOptions } from './users.class'
 import { userPath, userMethods } from './users.shared'
+import { authorize } from 'feathers-casl'
+import { defineAbilitiesFor } from '../authentication/authentication.abilities'
 
 export * from './users.class'
 export * from './users.schema'
@@ -30,27 +32,39 @@ export const user = (app: Application) => {
     // You can add additional custom events to be sent to clients here
     events: []
   })
+  const authorizeHook = authorize({ adapter: "@feathersjs/knex" });
   // Initialize hooks
   app.service(userPath).hooks({
     around: {
       all: [schemaHooks.resolveExternal(userExternalResolver), schemaHooks.resolveResult(userResolver)],
-      find: [authenticate('jwt')],
-      get: [authenticate('jwt')],
+      find: [authenticate('jwt'), authorizeHook],
+      get: [],
       create: [],
-      update: [authenticate('jwt')],
-      patch: [authenticate('jwt')],
-      remove: [authenticate('jwt')]
+      update: [authenticate('jwt'), authorizeHook],
+      patch: [authenticate('jwt'), authorizeHook],
+      remove: [authenticate('jwt'), authorizeHook]
     },
     before: {
       all: [schemaHooks.validateQuery(userQueryValidator), schemaHooks.resolveQuery(userQueryResolver)],
       find: [],
-      get: [],
+      get: [
+        authenticate('jwt'),
+        (context: HookContext) => {
+          if (context.params.ability) { return context; }
+          const { user } = context.params
+          if (user) context.params.ability = defineAbilitiesFor(user)
+          return context
+        },
+        authorizeHook,
+      ],
       create: [schemaHooks.validateData(userDataValidator), schemaHooks.resolveData(userDataResolver)],
+      update: [],
       patch: [schemaHooks.validateData(userPatchValidator), schemaHooks.resolveData(userPatchResolver)],
       remove: []
     },
     after: {
-      all: []
+      all: [],
+      get: [authorizeHook]
     },
     error: {
       all: []

+ 2 - 1
server/tsconfig.json

@@ -10,7 +10,8 @@
     "declaration": true,
     "strict": true,
     "esModuleInterop": true,
-    "sourceMap": true
+    "sourceMap": true,
+    "skipLibCheck": true
   },
   "include": [
     "src"