]> PHS Git Server - phs-api.git/commitdiff
Adding auth, media, and messaging.
authorcharleswrayjr <charleswrayjr@gmail.com>
Sat, 13 Sep 2025 06:32:17 +0000 (01:32 -0500)
committercharleswrayjr <charleswrayjr@gmail.com>
Sat, 13 Sep 2025 06:32:17 +0000 (01:32 -0500)
24 files changed:
.idea/data_source_mapping.xml [deleted file]
.idea/zencoder-chat-index.xml
package-lock.json
package.json
src/controllers/auth.controller.js
src/controllers/media.controller.js [new file with mode: 0644]
src/controllers/message.controller.js [new file with mode: 0644]
src/controllers/message_group.controller.js [new file with mode: 0644]
src/controllers/message_group_members.controller.js [new file with mode: 0644]
src/controllers/post.controller.js [new file with mode: 0644]
src/middleware/routeHelpers.js
src/models/index.js
src/models/media.model.js [new file with mode: 0644]
src/models/message.model.js [new file with mode: 0644]
src/models/message_group.model.js [new file with mode: 0644]
src/models/message_group_members.model.js [new file with mode: 0644]
src/models/post.model.js [new file with mode: 0644]
src/models/user.model.js
src/routes/auth.routes.js
src/routes/media.routes.js [new file with mode: 0644]
src/routes/message.routes.js [new file with mode: 0644]
src/routes/message_group.routes.js [new file with mode: 0644]
src/routes/message_group_members.routes.js [new file with mode: 0644]
src/routes/post.routes.js [new file with mode: 0644]

diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml
deleted file mode 100644 (file)
index 4b94a0b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="DataSourcePerFileMappings">
-    <file url="mock:///model.js" value="1dddfa52-f936-4314-90df-4fcc802f6908" />
-    <file url="mock:///model.js" value="1dddfa52-f936-4314-90df-4fcc802f6908" />
-    <file url="mock:///model.js" value="1dddfa52-f936-4314-90df-4fcc802f6908" />
-  </component>
-</project>
\ No newline at end of file
index 3708afada6758c086b9db78553ae36164cb26c5b..2009da499d2747bbdb52ec3c926efacc8d79b686 100644 (file)
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="ai.zencoder.plugin.chat.index">
-    <option name="activeChatId" value="744c3e10-33d2-414e-91ed-0472ce689be3" />
     <option name="chatMetadata" value="{&quot;744c3e10-33d2-414e-91ed-0472ce689be3&quot;:{&quot;id&quot;:&quot;744c3e10-33d2-414e-91ed-0472ce689be3&quot;,&quot;title&quot;:&quot;Postgres Model Definitions Update&quot;,&quot;createdAt&quot;:1757523010531,&quot;updatedAt&quot;:1757523030127,&quot;isAgent&quot;:true,&quot;isNameGenerated&quot;:true,&quot;messageCount&quot;:3,&quot;lastMessagePreview&quot;:&quot;**Modifying model structure**\n\nI need to adjust user.model.js to align with the new schema and creat...&quot;}}" />
   </component>
 </project>
\ No newline at end of file
index 0ec54a98f6ac86cce5b6390e99550ff0466c340a..a0b3a3bfba80d94a0c5519980d63ac73a285602f 100755 (executable)
@@ -8,10 +8,12 @@
       "name": "phs-api",
       "version": "1.0.0",
       "dependencies": {
+        "bcrypt": "^5.0.1",
         "body-parser": "^2.2.0",
         "cookie-parser": "~1.4.4",
         "cors": "^2.8.5",
         "debug": "~2.6.9",
+        "dockerode": "^4.0.7",
         "express": "5.0.0",
         "express-compression": "^1.0.2",
         "http-errors": "~1.6.3",
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@balena/dockerignore": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
+      "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
+    },
     "node_modules/@bcoe/v8-coverage": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
         "tslib": "^2.4.0"
       }
     },
+    "node_modules/@grpc/grpc-js": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
+      "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
+      "dependencies": {
+        "@grpc/proto-loader": "^0.7.13",
+        "@js-sdsl/ordered-map": "^4.4.2"
+      },
+      "engines": {
+        "node": ">=12.10.0"
+      }
+    },
+    "node_modules/@grpc/proto-loader": {
+      "version": "0.7.15",
+      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+      "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+      "dependencies": {
+        "lodash.camelcase": "^4.3.0",
+        "long": "^5.0.0",
+        "protobufjs": "^7.2.5",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@js-sdsl/ordered-map": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+      "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/js-sdsl"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+      "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "make-dir": "^3.1.0",
+        "node-fetch": "^2.6.7",
+        "nopt": "^5.0.0",
+        "npmlog": "^5.0.1",
+        "rimraf": "^3.0.2",
+        "semver": "^7.3.5",
+        "tar": "^6.1.11"
+      },
+      "bin": {
+        "node-pre-gyp": "bin/node-pre-gyp"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@napi-rs/wasm-runtime": {
       "version": "0.2.12",
       "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
         "url": "https://opencollective.com/pkgr"
       }
     },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+    },
     "node_modules/@scarf/scarf": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
       "version": "24.3.0",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
       "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
-      "dev": true,
       "dependencies": {
         "undici-types": "~7.10.0"
       }
         "win32"
       ]
     },
+    "node_modules/abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+    },
     "node_modules/accepts": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
         "node": ">=0.4.0"
       }
     },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/agent-base/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/agent-base/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
     "node_modules/ansi-escapes": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^2.0.1"
       },
       "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
       "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
     },
+    "node_modules/aproba": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+      "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="
+    },
+    "node_modules/are-we-there-yet": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+      "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
     },
     "node_modules/basic-auth": {
       "version": "2.0.1",
         "node": ">= 0.8"
       }
     },
+    "node_modules/bcrypt": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
+      "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@mapbox/node-pre-gyp": "^1.0.11",
+        "node-addon-api": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
     "node_modules/bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
         "tweetnacl": "^0.14.3"
       }
     },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/bl/node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
     "node_modules/body-parser": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
         "node-int64": "^0.4.0"
       }
     },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "node_modules/buffer-equal-constant-time": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
         "is-regex": "^1.0.3"
       }
     },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/ci-info": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
       "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-      "dev": true,
       "dependencies": {
         "string-width": "^4.2.0",
         "strip-ansi": "^6.0.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
     "node_modules/cliui/node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/cliui/node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "dependencies": {
         "color-name": "~1.1.4"
       },
     "node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/color-support": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+      "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+      "bin": {
+        "color-support": "bin.js"
+      }
     },
     "node_modules/compressible": {
       "version": "2.0.18",
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
     "node_modules/concat-stream": {
       "version": "2.0.0",
         "typedarray": "^0.0.6"
       }
     },
+    "node_modules/console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
+    },
     "node_modules/constantinople": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
         "node": ">=0.10.0"
       }
     },
+    "node_modules/delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
+    },
     "node_modules/depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
         "node": ">= 0.6"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+      "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/detect-newline": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/docker-modem": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
+      "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "readable-stream": "^3.5.0",
+        "split-ca": "^1.0.1",
+        "ssh2": "^1.15.0"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/docker-modem/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/docker-modem/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/dockerode": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.8.tgz",
+      "integrity": "sha512-HdPBprWmwfHMHi12AVIFDhXIqIS+EpiOVkZaAZxgML4xf5McqEZjJZtahTPkLDxWOt84ApfWPAH9EoQwOiaAIQ==",
+      "dependencies": {
+        "@balena/dockerignore": "^1.0.2",
+        "@grpc/grpc-js": "^1.11.1",
+        "@grpc/proto-loader": "^0.7.13",
+        "docker-modem": "^5.0.6",
+        "protobufjs": "^7.3.2",
+        "tar-fs": "~2.1.3",
+        "uuid": "^10.0.0"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
     "node_modules/doctypes": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
     "node_modules/error-ex": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
       "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
         "node": ">= 0.8"
       }
     },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
     "node_modules/fs-extra": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
         "node": ">=6 <7 || >=8"
       }
     },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
     },
     "node_modules/fsevents": {
       "version": "2.3.3",
         "darwin"
       ],
       "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gauge": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.2",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.1",
+        "object-assign": "^4.1.1",
+        "signal-exit": "^3.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/gauge/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/gauge/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/gauge/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=="
+    },
+    "node_modules/gauge/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/gauge/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
       }
     },
     "node_modules/gensync": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true,
       "engines": {
         "node": "6.* || 8.* || >= 10.*"
       }
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
+    },
     "node_modules/hasown": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
         "node": ">= 0.6"
       }
     },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
     "node_modules/human-signals": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/import-local": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
       "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
-      "dev": true,
       "dependencies": {
         "once": "^1.3.0",
         "wrappy": "1"
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
         "node": ">=8"
       }
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+    },
     "node_modules/lodash.includes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
     },
+    "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=="
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minizlib/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
     "node_modules/mkdirp": {
       "version": "0.5.6",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
         "mkdirp": "bin/cmd.js"
       }
     },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+    },
     "node_modules/morgan": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
         "node": ">= 0.6"
       }
     },
+    "node_modules/node-addon-api": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
       "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
       "dev": true
     },
+    "node_modules/nopt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/npmlog": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "are-we-there-yet": "^2.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^3.0.0",
+        "set-blocking": "^2.0.0"
+      }
+    },
     "node_modules/object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
         "asap": "~2.0.3"
       }
     },
+    "node_modules/protobufjs": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+      "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
       "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
       "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ=="
     },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
     "node_modules/pure-rand": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
       "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
       "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
     },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/rimraf/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/router": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
         "node": ">= 18"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
     "node_modules/setprototypeof": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
         "node": ">=14.0.0"
       }
     },
+    "node_modules/split-ca": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+      "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
+    },
     "node_modules/split2": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
         "url": "https://opencollective.com/synckit"
       }
     },
+    "node_modules/tar": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^5.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tar-fs": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+      "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
+      "dependencies": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "node_modules/tar-fs/node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+    },
+    "node_modules/tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tar/node_modules/minipass": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+      "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tar/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
     "node_modules/test-exclude": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
       "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
       "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
     },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
     "node_modules/tslib": {
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
     "node_modules/undici-types": {
       "version": "7.10.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
-      "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
-      "dev": true
+      "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
     },
     "node_modules/universalify": {
       "version": "0.1.2",
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/v8-to-istanbul": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
         "makeerror": "1.0.12"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
         "node": ">= 8"
       }
     },
+    "node_modules/wide-align": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+      "dependencies": {
+        "string-width": "^1.0.2 || 2 || 3 || 4"
+      }
+    },
+    "node_modules/wide-align/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wide-align/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/wide-align/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wide-align/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/with": {
       "version": "7.0.2",
       "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
       "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
-      "dev": true,
       "engines": {
         "node": ">=10"
       }
       "version": "17.7.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
       "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
-      "dev": true,
       "dependencies": {
         "cliui": "^8.0.1",
         "escalade": "^3.1.1",
       "version": "21.1.1",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
       "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       }
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
     "node_modules/yargs/node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/yargs/node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
index 4372ab0d2c2ddc9972a460a4e5b4937cd6be22be..b279d6ee4077ce97c073c23b5729193d46e08d18 100755 (executable)
@@ -6,6 +6,7 @@
     "start-debug": "NODE_ENV=\"DEVELOPMENT\" nodemon --trace-warnings --inspect=0.0.0.0:9229 app.js"
   },
   "dependencies": {
+    "bcrypt": "^5.0.1",
     "body-parser": "^2.2.0",
     "cookie-parser": "~1.4.4",
     "cors": "^2.8.5",
index bf429b6de11474e39ae5ac0bd2ebc471345303b7..0dcd1dd92ea2b6ec2ffeb50a325cd989375246c4 100644 (file)
@@ -1,5 +1,96 @@
+/**
+ * @file Authentication controller for handling authentication requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+
+const updateUserAndReturn = async (validUser, res) => {
+  const token = await validUser.createToken();
+  res.status(200).send({ success: true, user: validUser.to_safe_json(), token });
+};
+
 module.exports = {
-  login: (req, res, next) => {
-    console.log('login');
+  create: async (req, res, next) => {
+    const { user_id, password, password_confirmation } = req.body;
+    if (!user_id || !password || !password_confirmation || password !== password_confirmation) {
+      return next(createError(400, 'Invalid parameters: user_id, password, and password_confirmation are required and passwords must match.'));
+    }
+    try {
+      const user = await db.user.find_one({ id: user_id });
+      if (!user) {
+        return next(createError(400, 'There is no user with this id.'));
+      }
+      const existing = await db.authentication.find_by_user_id(user_id);
+      if (existing) {
+        return next(createError(400, 'Authentication for this user already exists.'));
+      }
+      await user.hashPassword(password);
+      const auth = await db.authentication.find_by_user_id(user_id);
+      res.status(200).send(auth.to_safe_json());
+    } catch (e) {
+      logger.error(`Create auth error: ${e.message}`);
+      next(e);
+    }
+  },
+
+  update: async (req, res, next) => {
+    try {
+      const auth = await db.authentication.find_one(req.params);
+      if (!auth) return next(createError(404, 'Authentication record not found'));
+      const updated = await auth.update(req.body);
+      res.status(200).send(updated.to_safe_json());
+    } catch (e) {
+      logger.error(`Update auth error: ${e.message}`);
+      next(createError(400, 'Invalid parameters.'));
+    }
+  },
+
+  show: async (req, res, next) => {
+    try {
+      const auth = await db.authentication.find_one(req.params);
+      if (!auth) return next(createError(404, 'Authentication record not found'));
+      res.status(200).send(auth.to_safe_json());
+    } catch (e) {
+      logger.error(`Show auth error: ${e.message}`);
+      next(e);
+    }
+  },
+
+  index: async (req, res, next) => {
+    try {
+      const auths = await db.authentication.find_many(req.query);
+      if (!auths.length) return next(createError(404, 'No authentication records found'));
+      res.status(200).send(auths.map(a => a.to_safe_json()));
+    } catch (e) {
+      logger.error(`Index auth error: ${e.message}`);
+      next(e);
+    }
+  },
+
+  authenticate: async (req, res, next) => {
+    try {
+      const { email, password } = req.body;
+      if (!email || !password) {
+        return next(createError(400, !email ? 'You must provide an email to login.' : 'You must provide a password to login.'));
+      }
+      const user = await db.user.find_by_email(email);
+      if (!user || !user.is_active || user.is_deleted) {
+        return res.status(401).send({ success: false, user: null, token: null });
+      }
+      const auth = await db.authentication.find_by_user_id(user.id);
+      if (auth.is_locked) {
+        return res.status(401).send({ message: 'Your account is locked due to suspicious activity. Please contact us to continue.' });
+      }
+      const isValid = await user.comparePassword(password);
+      if (!isValid) {
+        await user.failLogin();
+        return res.status(401).send({ success: false, user: null, token: null });
+      }
+      return updateUserAndReturn(user, res);
+    } catch (e) {
+      logger.error(`Authenticate error: ${e.message}`);
+      next(e);
+    }
   },
 };
\ No newline at end of file
diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js
new file mode 100644 (file)
index 0000000..d4fdde2
--- /dev/null
@@ -0,0 +1,128 @@
+/**
+ * @file Media controller for handling media-related API requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+
+/**
+ * Media controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Create a new media
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async create(req, res, next) {
+    try {
+      const media_data = req.body;
+      const media = await db.media.create(media_data);
+      res.json(media);
+    } catch (error) {
+      logger.error(`Create media error: ${error.message}`);
+      next(createError(error.status || 409, error.message));
+    }
+  },
+
+  /**
+   * Find media by file path
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_file_path(req, res, next) {
+    try {
+      const { file_path } = req.params;
+      const media = await db.media.find_by_file_path(file_path);
+      if (!media) return next(createError(404, 'Media not found'));
+      res.json(media);
+    } catch (error) {
+      logger.error(`Find media by file path error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find one media by ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_one(req, res, next) {
+    try {
+      const { id } = req.params;
+      const media = await db.media.find_one({ id: parseInt(id) });
+      if (!media) return next(createError(404, 'Media not found'));
+      res.json(media);
+    } catch (error) {
+      logger.error(`Find media error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find many media
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_many(req, res, next) {
+    try {
+      const { limit = '100', offset = '0', ...where } = req.query;
+      const medias = await db.media.find_many(where, [], null, parseInt(limit), parseInt(offset));
+      res.json(medias);
+    } catch (error) {
+      logger.error(`Find many media error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Update a media
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async update(req, res, next) {
+    try {
+      const { id } = req.params;
+      const media_data = req.body;
+      const media = await db.media.instance().find_one({ id: parseInt(id) });
+      if (!media) return next(createError(404, 'Media not found'));
+      const updated_media = await media.update(media_data);
+      res.json(updated_media);
+    } catch (error) {
+      logger.error(`Update media error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  },
+
+  /**
+   * Soft delete a media
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async soft_delete(req, res, next) {
+    try {
+      const { id } = req.params;
+      const { deleted_by_id } = req.body;
+      const media = await db.media.instance().find_one({ id: parseInt(id) });
+      if (!media) return next(createError(404, 'Media not found'));
+      const deleted_media = await media.soft_delete(deleted_by_id);
+      res.json(deleted_media);
+    } catch (error) {
+      logger.error(`Soft delete media error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  }
+};
\ No newline at end of file
diff --git a/src/controllers/message.controller.js b/src/controllers/message.controller.js
new file mode 100644 (file)
index 0000000..b72e7ce
--- /dev/null
@@ -0,0 +1,127 @@
+/**
+ * @file Message controller for handling message-related API requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+const logger = global.logger;
+
+/**
+ * Message controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Create a new message
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async create(req, res, next) {
+    try {
+      const message_data = req.body;
+      const message = await db.message.create(message_data);
+      res.json(message);
+    } catch (error) {
+      logger.error(`Create message error: ${error.message}`);
+      next(createError(error.status || 409, error.message));
+    }
+  },
+
+  /**
+   * Find messages by group ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_group_id(req, res, next) {
+    try {
+      const { group_id } = req.params;
+      const { limit = 100, offset = 0 } = req.query;
+      const messages = await db.message.find_by_group_id(parseInt(group_id), [], null, parseInt(limit), parseInt(offset));
+      res.json(messages);
+    } catch (error) {
+      logger.error(`Find messages by group ID error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find messages by recipient ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_recipient_id(req, res, next) {
+    try {
+      const { recipient_id } = req.params;
+      const { limit = 100, offset = 0 } = req.query;
+      const messages = await db.message.find_by_recipient_id(parseInt(recipient_id), [], null, parseInt(limit), parseInt(offset));
+      res.json(messages);
+    } catch (error) {
+      logger.error(`Find messages by recipient ID error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find one message by ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_one(req, res, next) {
+    try {
+      const { id } = req.params;
+      const message = await db.message.find_one({ id: parseInt(id) });
+      if (!message) return next(createError(404, 'Message not found'));
+      res.json(message);
+    }
+    catch (error) {
+      logger.error(`Find message error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find many messages
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_many(req, res, next) {
+    try {
+      const { limit = 100, offset = 0, ...where } = req.query;
+      const messages = await db.message.find_many(where, [], null, parseInt(limit), parseInt(offset));
+      res.json(messages);
+    } catch (error) {
+      logger.error(`Find many messages error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Mark a message as read
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async mark_as_read(req, res, next) {
+    try {
+      const { id } = req.params;
+      const message = await db.message.instance().find_one({ id: parseInt(id) });
+      if (!message) return next(createError(404, 'Message not found'));
+      const updated_message = await message.mark_as_read();
+      res.json(updated_message);
+    } catch (error) {
+      logger.error(`Mark message as read error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  }
+};
\ No newline at end of file
diff --git a/src/controllers/message_group.controller.js b/src/controllers/message_group.controller.js
new file mode 100644 (file)
index 0000000..55909d3
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * @file Message group controller for handling message group-related API requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+const logger = global.logger;
+
+/**
+ * Message group controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Create a new message group
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async create(req, res, next) {
+    try {
+      const message_group_data = req.body;
+      const message_group = await db.message_group.create(message_group_data);
+      res.json(message_group);
+    } catch (error) {
+      logger.error(`Create message group error: ${error.message}`);
+      next(createError(error.status || 409, error.message));
+    }
+  },
+
+  /**
+   * Find one message group by ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_one(req, res, next) {
+    try {
+      const { id } = req.params;
+      const message_group = await db.message_group.find_one({ id: parseInt(id) });
+      if (!message_group) return next(createError(404, 'Message group not found'));
+      res.json(message_group);
+    } catch (error) {
+      logger.error(`Find message group error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find many message groups
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_many(req, res, next) {
+    try {
+      const { limit = 100, offset = 0, ...where } = req.query;
+      const message_groups = await db.message_group.find_many(where, [], null, parseInt(limit), parseInt(offset));
+      res.json(message_groups);
+    } catch (error) {
+      logger.error(`Find many message groups error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Update a message group
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async update(req, res, next) {
+    try {
+      const { id } = req.params;
+      const message_group_data = req.body;
+      const message_group = await db.message_group.instance().find_one({ id: parseInt(id) });
+      if (!message_group) return next(createError(404, 'Message group not found'));
+      const updated_message_group = await message_group.update(message_group_data);
+      res.json(updated_message_group);
+    } catch (error) {
+      logger.error(`Update message group error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  }
+};
\ No newline at end of file
diff --git a/src/controllers/message_group_members.controller.js b/src/controllers/message_group_members.controller.js
new file mode 100644 (file)
index 0000000..1024871
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * @file Message group members controller for handling message group member-related API requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+const logger = global.logger;
+
+/**
+ * Message group members controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Add a message group member relation
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async add_relation(req, res, next) {
+    try {
+      const { group_id, user_id } = req.body;
+      const relation = await db.message_group_members.add_relation(group_id, user_id);
+      res.json(relation);
+    } catch (error) {
+      logger.error(`Add message group member relation error: ${error.message}`);
+      next(createError(error.status || 409, error.message));
+    }
+  },
+
+  /**
+   * Remove a message group member relation
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async remove_relation(req, res, next) {
+    try {
+      const { user_id, group_id } = req.body;
+      const relation = await db.message_group_members.remove_relation(user_id, group_id);
+      if (!relation) return next(createError(404, 'Relation not found'));
+      res.json(relation);
+    } catch (error) {
+      logger.error(`Remove message group member relation error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  },
+
+  /**
+   * Find message group member relation by IDs
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_ids(req, res, next) {
+    try {
+      const { group_id, user_id } = req.params;
+      const relation = await db.message_group_members.find_by_ids(parseInt(group_id), parseInt(user_id));
+      if (!relation) return next(createError(404, 'Relation not found'));
+      res.json(relation);
+    } catch (error) {
+      logger.error(`Find message group member by IDs error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find message group member relations by group ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_group_id(req, res, next) {
+    try {
+      const { group_id } = req.params;
+      const { limit = 100, offset = 0 } = req.query;
+      const relations = await db.message_group_members.find_by_group_id(parseInt(group_id), [], null, parseInt(limit), parseInt(offset));
+      res.json(relations);
+    } catch (error) {
+      logger.error(`Find message group members by group ID error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  }
+};
\ No newline at end of file
diff --git a/src/controllers/post.controller.js b/src/controllers/post.controller.js
new file mode 100644 (file)
index 0000000..09f21a2
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * @file Post controller for handling post-related API requests
+ */
+
+const db = require('../models');
+const createError = require('http-errors');
+const logger = global.logger;
+
+/**
+ * Post controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Create a new post
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async create(req, res, next) {
+    try {
+      const post_data = req.body;
+      const post = await db.post.create(post_data);
+      res.json(post);
+    } catch (error) {
+      logger.error(`Create post error: ${error.message}`);
+      next(createError(error.status || 409, error.message));
+    }
+  },
+
+  /**
+   * Find post by title
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_by_title(req, res, next) {
+    try {
+      const { title } = req.params;
+      const post = await db.post.find_by_title(title);
+      if (!post) return next(createError(404, 'Post not found'));
+      res.json(post);
+    } catch (error) {
+      logger.error(`Find post by title error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find one post by ID
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_one(req, res, next) {
+    try {
+      const { id } = req.params;
+      const post = await db.post.find_one({ id: parseInt(id) });
+      if (!post) return next(createError(404, 'Post not found'));
+      res.json(post);
+    } catch (error) {
+      logger.error(`Find post error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Find many posts
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async find_many(req, res, next) {
+    try {
+      const { limit = 100, offset = 0, ...where } = req.query;
+      const posts = await db.post.find_many(where, [], null, parseInt(limit), parseInt(offset));
+      res.json(posts);
+    } catch (error) {
+      logger.error(`Find many posts error: ${error.message}`);
+      next(createError(error.status || 500, error.message));
+    }
+  },
+
+  /**
+   * Update a post
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async update(req, res, next) {
+    try {
+      const { id } = req.params;
+      const post_data = req.body;
+      const post = await db.post.instance().find_one({ id: parseInt(id) });
+      if (!post) return next(createError(404, 'Post not found'));
+      const updated_post = await post.update(post_data);
+      res.json(updated_post);
+    } catch (error) {
+      logger.error(`Update post error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  },
+
+  /**
+   * Soft delete a post
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async soft_delete(req, res, next) {
+    try {
+      const { id } = req.params;
+      const { deleted_by_id } = req.body;
+      const post = await db.post.instance().find_one({ id: parseInt(id) });
+      if (!post) return next(createError(404, 'Post not found'));
+      const deleted_post = await post.soft_delete(deleted_by_id);
+      res.json(deleted_post);
+    } catch (error) {
+      logger.error(`Soft delete post error: ${error.message}`);
+      next(createError(error.status || 400, error.message));
+    }
+  }
+};
\ No newline at end of file
index 939138adb17375c6b48fb01ff9c8de1701806d63..9bcc4bab0db9e57c765b2dcbdd1af34bddfe8991 100755 (executable)
@@ -38,4 +38,11 @@ const allowApi = function (apiKey, callback) {
   return checkKey;
 };
 
-module.exports = { validateAuth, allowApi };
\ No newline at end of file
+const restrictToRoles = (roles) => (req, res, next) => {
+  if (!req.user || !req.user.roles.some(r => roles.includes(r))) {
+    return next(createError(403, 'Forbidden: Insufficient role permissions'));
+  }
+  next();
+};
+
+module.exports = { validateAuth, allowApi, restrictToRoles };
\ No newline at end of file
index d6d6f72f1448e0d33cab15e542d13cf50f9119c1..59e2c8eccba2edbbea1206e96f63717bae9eb5ae 100644 (file)
  */
 
 const model_cache = new Map();
-const { NotFoundError, FailedToCreateError, ValidationError } = require( './model' );
+const { NotFoundError } = require( './model' );
 
 /**
  * Get a model class from cache or require it
  * @param {string} name - Model name
  * @returns {Function} Model class
  */
-const get_model = ( name ) => {
-  if (!model_cache.has( name )) {
-    model_cache.set( name, require( `./${ name }.model` ) );
+const get_model = (name) => {
+  if (!model_cache.has(name)) {
+    model_cache.set(name, require(`./${name}.model`));
   }
-  return model_cache.get( name );
+  return model_cache.get(name);
 };
 
 /**
  * @returns {Function} User model class
  */
-const get_user_model = () => get_model( 'user' );
+const get_user_model = () => get_model('user');
 
 /**
  * @returns {Function} Phone number model class
  */
-const get_phone_number_model = () => get_model( 'phone_number' );
+const get_phone_number_model = () => get_model('phone_number');
 
 /**
  * @returns {Function} User phone numbers model class
  */
-const get_user_phone_numbers_model = () => get_model( 'user_phone_numbers' );
+const get_user_phone_numbers_model = () => get_model('user_phone_numbers');
 
 /**
  * @returns {Function} Address model class
  */
-const get_address_model = () => get_model( 'address' );
+const get_address_model = () => get_model('address');
 
 /**
  * @returns {Function} User addresses model class
  */
-const get_user_addresses_model = () => get_model( 'user_addresses' );
+const get_user_addresses_model = () => get_model('user_addresses');
 
 /**
  * @returns {Function} Authentication model class
  */
-const get_authentication_model = () => get_model( 'authentication' );
+const get_authentication_model = () => get_model('authentication');
 
 /**
  * @returns {Function} Role model class
  */
-const get_role_model = () => get_model( 'role' );
+const get_role_model = () => get_model('role');
 
 /**
  * @returns {Function} User roles model class
  */
-const get_user_roles_model = () => get_model( 'user_roles' );
+const get_user_roles_model = () => get_model('user_roles');
+
+/**
+ * @returns {Function} Media model class
+ */
+const get_media_model = () => get_model('media');
+
+/**
+ * @returns {Function} Post model class
+ */
+const get_post_model = () => get_model('post');
+
+/**
+ * @returns {Function} Message group model class
+ */
+const get_message_group_model = () => get_model('message_group');
+
+/**
+ * @returns {Function} Message group members model class
+ */
+const get_message_group_members_model = () => get_model('message_group_members');
+
+/**
+ * @returns {Function} Message model class
+ */
+const get_message_model = () => get_model('message');
 
 /**
  * Unified database interface
  * @type {Object}
  */
 const db = {
-  user:{
-    /**
-     * Create a new user
-     * @param {Object} user_data - User data
-     * @returns {Promise<User>} Created user instance
-     * @throws {ValidationError} If required fields are missing
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async create( user_data ) {
+  user: {
+    async create(user_data) {
       try {
-        return await get_user_model().create( user_data );
+        return await get_user_model().create(user_data);
       } catch (error) {
-        logger.error( `Failed to create user: ${ error.message }` );
+        logger.error(`Failed to create user: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user by email
-     * @param {string} email - Email to search for
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<User|null>} User instance or null
-     */
-    async find_by_email( email, excludes ) {
+    async find_by_email(email, excludes) {
       try {
-        return await get_user_model().find_by_email( email, excludes );
+        return await get_user_model().find_by_email(email, excludes);
       } catch (error) {
-        logger.error( `Failed to find user by email: ${ error.message }` );
+        logger.error(`Failed to find user by email: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user by nickname
-     * @param {string} nickname - Nickname to search for
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<User|null>} User instance or null
-     */
-    async find_by_nickname( nickname, excludes ) {
+    async find_by_nickname(nickname, excludes) {
       try {
-        return await get_user_model().find_by_nickname( nickname, excludes );
+        return await get_user_model().find_by_nickname(nickname, excludes);
       } catch (error) {
-        logger.error( `Failed to find user by nickname: ${ error.message }` );
+        logger.error(`Failed to find user by nickname: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find active users
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<User[]>} Array of active users
-     */
-    async find_active( excludes, order_by, limit, offset ) {
+    async find_active(excludes, order_by, limit, offset) {
       try {
-        return await get_user_model().find_active( excludes, order_by, limit, offset );
+        return await get_user_model().find_active(excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find active users: ${ error.message }` );
+        logger.error(`Failed to find active users: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find deleted users
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<User[]>} Array of deleted users
-     */
-    async find_deleted( excludes, order_by, limit, offset ) {
+    async find_deleted(excludes, order_by, limit, offset) {
       try {
-        return await get_user_model().find_deleted( excludes, order_by, limit, offset );
+        return await get_user_model().find_deleted(excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find deleted users: ${ error.message }` );
+        logger.error(`Failed to find deleted users: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find one user
-     * @param {Object} where - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<User|null>} User instance or null
-     */
-    async find_one( where, excludes ) {
+    async find_one(where, excludes) {
       try {
-        return await new (get_user_model())().find_one( where, excludes );
+        return await new (get_user_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to find one user: ${ error.message }` );
+        logger.error(`Failed to find one user: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find many users
-     * @param {Object} [where] - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<User[]>} Array of users
-     */
-    async find_many( where, excludes, order_by, limit, offset ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await new (get_user_model())().find_many( where, excludes, order_by, limit, offset );
+        return await new (get_user_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find many users: ${ error.message }` );
+        logger.error(`Failed to find many users: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Deactivate user
-     * @param {number|string} id - User ID
-     * @param {number|string} deactivated_by_id - ID of user performing deactivation
-     * @returns {Promise<User>} Updated user instance
-     * @throws {NotFoundError} If user not found
-     */
-    async deactivate( id, deactivated_by_id ) {
+    async deactivate(id, deactivated_by_id) {
       try {
-        const user = await new (get_user_model())().find_one( { id }, [] );
-        if (!user) throw new NotFoundError( 'User not found' );
-        return await user.deactivate( deactivated_by_id );
+        const user = await new (get_user_model())().find_one({ id }, []);
+        if (!user) throw new NotFoundError('User not found');
+        return await user.deactivate(deactivated_by_id);
       } catch (error) {
-        logger.error( `Failed to deactivate user: ${ error.message }` );
+        logger.error(`Failed to deactivate user: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Reactivate user
-     * @param {number|string} id - User ID
-     * @returns {Promise<User>} Updated user instance
-     * @throws {NotFoundError} If user not found
-     */
-    async reactivate( id ) {
+    async reactivate(id) {
       try {
-        const user = await new (get_user_model())().find_one( { id }, [] );
-        if (!user) throw new NotFoundError( 'User not found' );
+        const user = await new (get_user_model())().find_one({ id }, []);
+        if (!user) throw new NotFoundError('User not found');
         return await user.reactivate();
       } catch (error) {
-        logger.error( `Failed to reactivate user: ${ error.message }` );
+        logger.error(`Failed to reactivate user: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Soft delete user
-     * @param {number|string} id - User ID
-     * @param {number|string} deleted_by_id - ID of user performing deletion
-     * @returns {Promise<User>} Updated user instance
-     * @throws {NotFoundError} If user not found
-     */
-    async soft_delete( id, deleted_by_id ) {
+    async soft_delete(id, deleted_by_id) {
       try {
-        const user = await new (get_user_model())().find_one( { id }, [] );
-        if (!user) throw new NotFoundError( 'User not found' );
-        return await user.soft_delete( deleted_by_id );
+        const user = await new (get_user_model())().find_one({ id }, []);
+        if (!user) throw new NotFoundError('User not found');
+        return await user.soft_delete(deleted_by_id);
       } catch (error) {
-        logger.error( `Failed to soft delete user: ${ error.message }` );
+        logger.error(`Failed to soft delete user: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new User instance
-     * @returns {User} User instance
-     */
-    instance:() => new (get_user_model())()
+    instance: () => new (get_user_model())()
   },
-  phone_number:{
-    /**
-     * Create a new phone number
-     * @param {Object} phone_data - Phone number data
-     * @returns {Promise<PhoneNumber>} Created phone number instance
-     * @throws {ValidationError} If required fields are missing
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async create( phone_data ) {
+  phone_number: {
+    async create(phone_data) {
       try {
-        return await get_phone_number_model().create( phone_data );
+        return await get_phone_number_model().create(phone_data);
       } catch (error) {
-        logger.error( `Failed to create phone number: ${ error.message }` );
+        logger.error(`Failed to create phone number: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find phone number by number
-     * @param {string} number - Phone number to search for
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<PhoneNumber|null>} Phone number instance or null
-     */
-    async find_by_number( number, excludes ) {
+    async find_by_number(number, excludes) {
       try {
-        return await get_phone_number_model().find_by_number( number, excludes );
+        return await get_phone_number_model().find_by_number(number, excludes);
       } catch (error) {
-        logger.error( `Failed to find phone number: ${ error.message }` );
+        logger.error(`Failed to find phone number: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find one phone number
-     * @param {Object} where - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<PhoneNumber|null>} Phone number instance or null
-     */
-    async find_one( where, excludes ) {
+    async find_one(where, excludes) {
       try {
-        return await new (get_phone_number_model())().find_one( where, excludes );
+        return await new (get_phone_number_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to find one phone number: ${ error.message }` );
+        logger.error(`Failed to find one phone number: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find many phone numbers
-     * @param {Object} [where] - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<PhoneNumber[]>} Array of phone numbers
-     */
-    async find_many( where, excludes, order_by, limit, offset ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await new (get_phone_number_model())().find_many( where, excludes, order_by, limit, offset );
+        return await new (get_phone_number_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find many phone numbers: ${ error.message }` );
+        logger.error(`Failed to find many phone numbers: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new PhoneNumber instance
-     * @returns {PhoneNumber} PhoneNumber instance
-     */
-    instance:() => new (get_phone_number_model())()
+    instance: () => new (get_phone_number_model())()
   },
-  user_phone_numbers:{
-    /**
-     * Add a user-phone number relation
-     * @param {number|string} user_id - User ID
-     * @param {number|string} phone_number_id - Phone number ID
-     * @returns {Promise<UserPhoneNumber>} Created relation instance
-     * @throws {ValidationError} If IDs are invalid
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async add_relation( user_id, phone_number_id ) {
+  user_phone_numbers: {
+    async add_relation(user_id, phone_number_id) {
       try {
-        return await get_user_phone_numbers_model().add_relation( user_id, phone_number_id );
+        return await get_user_phone_numbers_model().add_relation(user_id, phone_number_id);
       } catch (error) {
-        logger.error( `Failed to add user-phone number relation: ${ error.message }` );
+        logger.error(`Failed to add user-phone number relation: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Remove a user-phone number relation
-     * @param {number|string} phone_number_id - Phone number ID
-     * @param {number|string|undefined} [user_id] - User ID
-     * @returns {Promise<UserPhoneNumber|null>} Deleted relation instance or null
-     * @throws {ValidationError} If IDs are invalid
-     */
-    async remove_relation( phone_number_id, user_id ) {
+    async remove_relation(phone_number_id, user_id) {
       try {
-        return await get_user_phone_numbers_model().remove_relation( phone_number_id, user_id );
+        return await get_user_phone_numbers_model().remove_relation(phone_number_id, user_id);
       } catch (error) {
-        logger.error( `Failed to remove user-phone number relation: ${ error.message }` );
+        logger.error(`Failed to remove user-phone number relation: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-phone number relation by IDs
-     * @param {number|string} user_id - User ID
-     * @param {number|string} phone_number_id - Phone number ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<UserPhoneNumber|null>} Relation instance or null
-     */
-    async find_by_ids( user_id, phone_number_id, excludes ) {
+    async find_by_ids(user_id, phone_number_id, excludes) {
       try {
-        return await get_user_phone_numbers_model().find_by_ids( user_id, phone_number_id, excludes );
+        return await get_user_phone_numbers_model().find_by_ids(user_id, phone_number_id, excludes);
       } catch (error) {
-        logger.error( `Failed to find user-phone number by IDs: ${ error.message }` );
+        logger.error(`Failed to find user-phone number by IDs: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-phone number relations by user ID
-     * @param {number|string} user_id - User ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<UserPhoneNumber[]>} Array of relations
-     */
-    async find_by_user_id( user_id, excludes, order_by, limit, offset ) {
+    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
       try {
-        return await get_user_phone_numbers_model().find_by_user_id( user_id, excludes, order_by, limit, offset );
+        return await get_user_phone_numbers_model().find_by_user_id(user_id, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find user-phone numbers by user ID: ${ error.message }` );
+        logger.error(`Failed to find user-phone numbers by user ID: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new UserPhoneNumber instance
-     * @returns {UserPhoneNumber} UserPhoneNumber instance
-     */
-    instance:() => new (get_user_phone_numbers_model())()
+    instance: () => new (get_user_phone_numbers_model())()
   },
-  address:{
-    /**
-     * Create a new address
-     * @param {Object} address_data - Address data
-     * @returns {Promise<Address>} Created address instance
-     * @throws {ValidationError} If required fields are missing
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async create( address_data ) {
+  address: {
+    async create(address_data) {
       try {
-        return await get_address_model().create( address_data );
+        return await get_address_model().create(address_data);
       } catch (error) {
-        logger.error( `Failed to create address: ${ error.message }` );
+        logger.error(`Failed to create address: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find address by zip code
-     * @param {string} zip_code - Zip code to search for
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Address|null>} Address instance or null
-     */
-    async find_by_zip_code( zip_code, excludes ) {
+    async find_by_zip_code(zip_code, excludes) {
       try {
-        return await get_address_model().find_by_zip_code( zip_code, excludes );
+        return await get_address_model().find_by_zip_code(zip_code, excludes);
       } catch (error) {
-        logger.error( `Failed to find address by zip code: ${ error.message }` );
+        logger.error(`Failed to find address by zip code: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find one address
-     * @param {Object} where - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Address|null>} Address instance or null
-     */
-    async find_one( where, excludes ) {
+    async find_one(where, excludes) {
       try {
-        return await new (get_address_model())().find_one( where, excludes );
+        return await new (get_address_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to find one address: ${ error.message }` );
+        logger.error(`Failed to find one address: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find many addresses
-     * @param {Object} [where] - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<Address[]>} Array of addresses
-     */
-    async find_many( where, excludes, order_by, limit, offset ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await new (get_address_model())().find_many( where, excludes, order_by, limit, offset );
+        return await new (get_address_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find many addresses: ${ error.message }` );
+        logger.error(`Failed to find many addresses: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new Address instance
-     * @returns {Address} Address instance
-     */
-    instance:() => new (get_address_model())()
+    instance: () => new (get_address_model())()
   },
-  user_addresses:{
-    /**
-     * Add a user-address relation
-     * @param {number|string} user_id - User ID
-     * @param {number|string} address_id - Address ID
-     * @returns {Promise<UserAddress>} Created relation instance
-     * @throws {ValidationError} If IDs are invalid
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async add_relation( user_id, address_id ) {
+  user_addresses: {
+    async add_relation(user_id, address_id) {
       try {
-        return await get_user_addresses_model().add_relation( user_id, address_id );
+        return await get_user_addresses_model().add_relation(user_id, address_id);
       } catch (error) {
-        logger.error( `Failed to add user-address relation: ${ error.message }` );
+        logger.error(`Failed to add user-address relation: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Remove a user-address relation
-     * @param {number|string} address_id - Address ID
-     * @param {number|string|undefined} [user_id] - User ID
-     * @returns {Promise<UserAddress|null>} Deleted relation instance or null
-     * @throws {ValidationError} If IDs are invalid
-     */
-    async remove_relation( address_id, user_id ) {
+    async remove_relation(address_id, user_id) {
       try {
-        return await get_user_addresses_model().remove_relation( address_id, user_id );
+        return await get_user_addresses_model().remove_relation(address_id, user_id);
       } catch (error) {
-        logger.error( `Failed to remove user-address relation: ${ error.message }` );
+        logger.error(`Failed to remove user-address relation: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-address relation by IDs
-     * @param {number|string} user_id - User ID
-     * @param {number|string} address_id - Address ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<UserAddress|null>} Relation instance or null
-     */
-    async find_by_ids( user_id, address_id, excludes ) {
+    async find_by_ids(user_id, address_id, excludes) {
       try {
-        return await get_user_addresses_model().find_by_ids( user_id, address_id, excludes );
+        return await get_user_addresses_model().find_by_ids(user_id, address_id, excludes);
       } catch (error) {
-        logger.error( `Failed to find user-address by IDs: ${ error.message }` );
+        logger.error(`Failed to find user-address by IDs: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-address relations by user ID
-     * @param {number|string} user_id - User ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<UserAddress[]>} Array of relations
-     */
-    async find_by_user_id( user_id, excludes, order_by, limit, offset ) {
+    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
       try {
-        return await get_user_addresses_model().find_by_user_id( user_id, excludes, order_by, limit, offset );
+        return await get_user_addresses_model().find_by_user_id(user_id, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find user-addresses by user ID: ${ error.message }` );
+        logger.error(`Failed to find user-addresses by user ID: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new UserAddress instance
-     * @returns {UserAddress} UserAddress instance
-     */
-    instance:() => new (get_user_addresses_model())()
+    instance: () => new (get_user_addresses_model())()
   },
-  authentication:{
-    /**
-     * Create a new authentication record
-     * @param {Object} auth_data - Authentication data
-     * @returns {Promise<Authentication>} Created authentication instance
-     * @throws {ValidationError} If required fields are missing
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async create( auth_data ) {
+  authentication: {
+    async create(auth_data) {
       try {
-        return await get_authentication_model().create( auth_data );
+        return await get_authentication_model().create(auth_data);
       } catch (error) {
-        logger.error( `Failed to create authentication record: ${ error.message }` );
+        logger.error(`Failed to create authentication record: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find authentication record by user ID
-     * @param {number|string} user_id - User ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Authentication|null>} Authentication instance or null
-     */
-    async find_by_user_id( user_id, excludes ) {
+    async find_by_user_id(user_id, excludes) {
       try {
-        return await get_authentication_model().find_by_user_id( user_id, excludes );
+        return await get_authentication_model().find_by_user_id(user_id, excludes);
       } catch (error) {
-        logger.error( `Failed to find authentication by user ID: ${ error.message }` );
+        logger.error(`Failed to find authentication by user ID: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find authentication record by reset token
-     * @param {string} token - Password reset token
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Authentication|null>} Authentication instance or null
-     */
-    async find_by_reset_token( token, excludes ) {
+    async find_by_reset_token(token, excludes) {
       try {
-        return await get_authentication_model().find_by_reset_token( token, excludes );
+        return await get_authentication_model().find_by_reset_token(token, excludes);
       } catch (error) {
-        logger.error( `Failed to find authentication by reset token: ${ error.message }` );
+        logger.error(`Failed to find authentication by reset token: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find one authentication record
-     * @param {Object} where - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Authentication|null>} Authentication instance or null
-     */
-    async find_one( where, excludes ) {
+    async find_one(where, excludes) {
       try {
-        return await new (get_authentication_model())().find_one( where, excludes );
+        return await new (get_authentication_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to find one authentication record: ${ error.message }` );
+        logger.error(`Failed to find one authentication record: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find many authentication records
-     * @param {Object} [where] - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<Authentication[]>} Array of authentication records
-     */
-    async find_many( where, excludes, order_by, limit, offset ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await new (get_authentication_model())().find_many( where, excludes, order_by, limit, offset );
+        return await new (get_authentication_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find many authentication records: ${ error.message }` );
+        logger.error(`Failed to find many authentication records: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Lock authentication account
-     * @param {number|string} id - Authentication ID
-     * @returns {Promise<Authentication>} Updated authentication instance
-     * @throws {NotFoundError} If authentication record not found
-     */
-    async lock_account( id ) {
+    async lock_account(id) {
       try {
-        const auth = await new (get_authentication_model())().find_one( { id }, [] );
-        if (!auth) throw new NotFoundError( 'Authentication record not found' );
+        const auth = await new (get_authentication_model())().find_one({ id }, []);
+        if (!auth) throw new NotFoundError('Authentication record not found');
         return await auth.lock_account();
       } catch (error) {
-        logger.error( `Failed to lock account: ${ error.message }` );
+        logger.error(`Failed to lock account: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Unlock authentication account
-     * @param {number|string} id - Authentication ID
-     * @returns {Promise<Authentication>} Updated authentication instance
-     * @throws {NotFoundError} If authentication record not found
-     */
-    async unlock_account( id ) {
+    async unlock_account(id) {
       try {
-        const auth = await new (get_authentication_model())().find_one( { id }, [] );
-        if (!auth) throw new NotFoundError( 'Authentication record not found' );
+        const auth = await new (get_authentication_model())().find_one({ id }, []);
+        if (!auth) throw new NotFoundError('Authentication record not found');
         return await auth.unlock_account();
       } catch (error) {
-        logger.error( `Failed to unlock account: ${ error.message }` );
+        logger.error(`Failed to unlock account: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Soft delete authentication record
-     * @param {number|string} id - Authentication ID
-     * @param {number|string} deleted_by_id - ID of user performing deletion
-     * @returns {Promise<Authentication>} Updated authentication instance
-     * @throws {NotFoundError} If authentication record not found
-     */
-    async soft_delete( id, deleted_by_id ) {
+    async soft_delete(id, deleted_by_id) {
       try {
-        const auth = await new (get_authentication_model())().find_one( { id }, [] );
-        if (!auth) throw new NotFoundError( 'Authentication record not found' );
-        return await auth.soft_delete( deleted_by_id );
+        const auth = await new (get_authentication_model())().find_one({ id }, []);
+        if (!auth) throw new NotFoundError('Authentication record not found');
+        return await auth.soft_delete(deleted_by_id);
       } catch (error) {
-        logger.error( `Failed to soft delete authentication: ${ error.message }` );
+        logger.error(`Failed to soft delete authentication: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new Authentication instance
-     * @returns {Authentication} Authentication instance
-     */
-    instance:() => new (get_authentication_model())()
+    instance: () => new (get_authentication_model())()
   },
-  role:{
-    /**
-     * Create a new role
-     * @param {Object} role_data - Role data
-     * @returns {Promise<Role>} Created role instance
-     * @throws {ValidationError} If required fields are missing
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async create( role_data ) {
+  role: {
+    async create(role_data) {
       try {
-        return await get_role_model().create( role_data );
+        return await get_role_model().create(role_data);
       } catch (error) {
-        logger.error( `Failed to create role: ${ error.message }` );
+        logger.error(`Failed to create role: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find role by name
-     * @param {string} name - Role name to search for
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Role|null>} Role instance or null
-     */
-    async find_by_name( name, excludes ) {
+    async find_by_name(name, excludes) {
       try {
-        return await get_role_model().find_by_name( name, excludes );
+        return await get_role_model().find_by_name(name, excludes);
       } catch (error) {
-        logger.error( `Failed to find role by name: ${ error.message }` );
+        logger.error(`Failed to find role by name: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find one role
-     * @param {Object} where - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<Role|null>} Role instance or null
-     */
-    async find_one( where, excludes ) {
+    async find_one(where, excludes) {
       try {
-        return await new (get_role_model())().find_one( where, excludes );
+        return await new (get_role_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to find one role: ${ error.message }` );
+        logger.error(`Failed to find one role: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find many roles
-     * @param {Object} [where] - Conditions
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<Role[]>} Array of roles
-     */
-    async find_many( where, excludes, order_by, limit, offset ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await new (get_role_model())().find_many( where, excludes, order_by, limit, offset );
+        return await new (get_role_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find many roles: ${ error.message }` );
+        logger.error(`Failed to find many roles: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new Role instance
-     * @returns {Role} Role instance
-     */
-    instance:() => new (get_role_model())()
+    instance: () => new (get_role_model())()
   },
-  user_roles:{
-    /**
-     * Add a user-role relation
-     * @param {number|string} user_id - User ID
-     * @param {number|string} role_id - Role ID
-     * @returns {Promise<UserRole>} Created relation instance
-     * @throws {ValidationError} If IDs are invalid
-     * @throws {FailedToCreateError} If creation fails
-     */
-    async add_relation( user_id, role_id ) {
+  user_roles: {
+    async add_relation(user_id, role_id) {
+      try {
+        return await get_user_roles_model().add_relation(user_id, role_id);
+      } catch (error) {
+        logger.error(`Failed to add user-role relation: ${error.message}`);
+        throw error;
+      }
+    },
+    async remove_relation(role_id, user_id) {
+      try {
+        return await get_user_roles_model().remove_relation(role_id, user_id);
+      } catch (error) {
+        logger.error(`Failed to remove user-role relation: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_ids(user_id, role_id, excludes) {
+      try {
+        return await get_user_roles_model().find_by_ids(user_id, role_id, excludes);
+      } catch (error) {
+        logger.error(`Failed to find user-role by IDs: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
+      try {
+        return await get_user_roles_model().find_by_user_id(user_id, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find user-roles by user ID: ${error.message}`);
+        throw error;
+      }
+    },
+    instance: () => new (get_user_roles_model())()
+  },
+  media: {
+    async create(media_data) {
+      try {
+        return await get_media_model().create(media_data);
+      } catch (error) {
+        logger.error(`Failed to create media: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_file_path(file_path, excludes) {
+      try {
+        return await get_media_model().find_by_file_path(file_path, excludes);
+      } catch (error) {
+        logger.error(`Failed to find media by file path: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_one(where, excludes) {
+      try {
+        return await new (get_media_model())().find_one(where, excludes);
+      } catch (error) {
+        logger.error(`Failed to find one media: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_many(where, excludes, order_by, limit, offset) {
+      try {
+        return await new (get_media_model())().find_many(where, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find many media: ${error.message}`);
+        throw error;
+      }
+    },
+    instance: () => new (get_media_model())()
+  },
+  post: {
+    async create(post_data) {
+      try {
+        return await get_post_model().create(post_data);
+      } catch (error) {
+        logger.error(`Failed to create post: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_title(title, excludes) {
+      try {
+        return await get_post_model().find_by_title(title, excludes);
+      } catch (error) {
+        logger.error(`Failed to find post by title: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_one(where, excludes) {
+      try {
+        return await new (get_post_model())().find_one(where, excludes);
+      } catch (error) {
+        logger.error(`Failed to find one post: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_many(where, excludes, order_by, limit, offset) {
+      try {
+        return await new (get_post_model())().find_many(where, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find many posts: ${error.message}`);
+        throw error;
+      }
+    },
+    instance: () => new (get_post_model())()
+  },
+  message_group: {
+    async create(message_group_data) {
+      try {
+        return await get_message_group_model().create(message_group_data);
+      } catch (error) {
+        logger.error(`Failed to create message group: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_one(where, excludes) {
+      try {
+        return await new (get_message_group_model())().find_one(where, excludes);
+      } catch (error) {
+        logger.error(`Failed to find one message group: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_many(where, excludes, order_by, limit, offset) {
+      try {
+        return await new (get_message_group_model())().find_many(where, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find many message groups: ${error.message}`);
+        throw error;
+      }
+    },
+    instance: () => new (get_message_group_model())()
+  },
+  message_group_members: {
+    async add_relation(group_id, user_id) {
+      try {
+        return await get_message_group_members_model().add_relation(group_id, user_id);
+      } catch (error) {
+        logger.error(`Failed to add message group member relation: ${error.message}`);
+        throw error;
+      }
+    },
+    async remove_relation(user_id, group_id) {
+      try {
+        return await get_message_group_members_model().remove_relation(user_id, group_id);
+      } catch (error) {
+        logger.error(`Failed to remove message group member relation: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_ids(group_id, user_id, excludes) {
+      try {
+        return await get_message_group_members_model().find_by_ids(group_id, user_id, excludes);
+      } catch (error) {
+        logger.error(`Failed to find message group member by IDs: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_group_id(group_id, excludes, order_by, limit, offset) {
+      try {
+        return await get_message_group_members_model().find_by_group_id(group_id, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find message group members by group ID: ${error.message}`);
+        throw error;
+      }
+    },
+    instance: () => new (get_message_group_members_model())()
+  },
+  message: {
+    async create(message_data) {
+      try {
+        return await get_message_model().create(message_data);
+      } catch (error) {
+        logger.error(`Failed to create message: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_group_id(group_id, excludes, order_by, limit, offset) {
+      try {
+        return await get_message_model().find_by_group_id(group_id, excludes, order_by, limit, offset);
+      } catch (error) {
+        logger.error(`Failed to find messages by group ID: ${error.message}`);
+        throw error;
+      }
+    },
+    async find_by_recipient_id(recipient_id, excludes, order_by, limit, offset) {
       try {
-        return await get_user_roles_model().add_relation( user_id, role_id );
+        return await get_message_model().find_by_recipient_id(recipient_id, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to add user-role relation: ${ error.message }` );
+        logger.error(`Failed to find messages by recipient ID: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Remove a user-role relation
-     * @param {number|string} role_id - Role ID
-     * @param {number|string|undefined} [user_id] - User ID
-     * @returns {Promise<UserRole|null>} Deleted relation instance or null
-     * @throws {ValidationError} If IDs are invalid
-     */
-    async remove_relation( role_id, user_id ) {
+    async find_one(where, excludes) {
       try {
-        return await get_user_roles_model().remove_relation( role_id, user_id );
+        return await new (get_message_model())().find_one(where, excludes);
       } catch (error) {
-        logger.error( `Failed to remove user-role relation: ${ error.message }` );
+        logger.error(`Failed to find one message: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-role relation by IDs
-     * @param {number|string} user_id - User ID
-     * @param {number|string} role_id - Role ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @returns {Promise<UserRole|null>} Relation instance or null
-     */
-    async find_by_ids( user_id, role_id, excludes ) {
+    async find_many(where, excludes, order_by, limit, offset) {
       try {
-        return await get_user_roles_model().find_by_ids( user_id, role_id, excludes );
+        return await new (get_message_model())().find_many(where, excludes, order_by, limit, offset);
       } catch (error) {
-        logger.error( `Failed to find user-role by IDs: ${ error.message }` );
+        logger.error(`Failed to find many messages: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Find user-role relations by user ID
-     * @param {number|string} user_id - User ID
-     * @param {string[]} [excludes] - Fields to exclude
-     * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-     * @param {number} [limit] - Maximum number of records
-     * @param {number} [offset] - Number of records to skip
-     * @returns {Promise<UserRole[]>} Array of relations
-     */
-    async find_by_user_id( user_id, excludes, order_by, limit, offset ) {
+    async mark_as_read(id) {
       try {
-        return await get_user_roles_model().find_by_user_id( user_id, excludes, order_by, limit, offset );
+        const message = await new (get_message_model())().find_one({ id }, []);
+        if (!message) throw new NotFoundError('Message not found');
+        return await message.mark_as_read();
       } catch (error) {
-        logger.error( `Failed to find user-roles by user ID: ${ error.message }` );
+        logger.error(`Failed to mark message as read: ${error.message}`);
         throw error;
       }
     },
-    /**
-     * Create a new UserRole instance
-     * @returns {UserRole} UserRole instance
-     */
-    instance:() => new (get_user_roles_model())()
+    instance: () => new (get_message_model())()
   }
 };
 
diff --git a/src/models/media.model.js b/src/models/media.model.js
new file mode 100644 (file)
index 0000000..586b110
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * @file Media model for phase.media table
+ */
+
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model');
+
+/**
+ * @typedef {Object} Media
+ * @property {number} id - Media ID (primary key)
+ * @property {number} user_id - User ID (foreign key)
+ * @property {string} file_path - File path
+ * @property {string} file_type - File type (e.g., 'image', 'video')
+ * @property {string} visibility - Visibility (e.g., 'private', 'family', 'public')
+ * @property {number|null} created_by_id - ID of user who created this media
+ * @property {Date} created_at - Creation timestamp
+ * @property {boolean} is_deleted - Soft delete flag
+ * @property {number|null} deleted_by_id - ID of user who deleted this media
+ * @property {Date|null} deleted_at - Deletion timestamp
+ */
+
+/**
+ * Media model class
+ * @extends Model
+ */
+class Media extends Model {
+  /**
+   * Create a Media instance
+   * @param {Partial<Media>} [props] - Media properties
+   */
+  constructor(props) {
+    super(props);
+    this.table = 'phase.media';
+    this.prepend = 'm';
+    this.default_columns = [
+      'id', 'user_id', 'file_path', 'file_type', 'visibility',
+      'created_by_id', 'created_at', 'is_deleted', 'deleted_by_id', 'deleted_at'
+    ];
+    this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id'];
+    this.base_query = `
+      SELECT m.id, m.user_id, m.file_path, m.file_type, m.visibility,
+             m.created_by_id, m.created_at, m.is_deleted, m.deleted_by_id, m.deleted_at
+      FROM phase.media m
+      WHERE m.is_deleted = false
+    `;
+    this.base_list_query = `
+      SELECT m.id, m.user_id, m.file_path, m.file_type, m.visibility,
+             m.created_by_id, m.created_at
+      FROM phase.media m
+      WHERE m.is_deleted = false
+    `;
+    this.default_order_by = 'ORDER BY m.created_at DESC';
+    this.instance = _props => new Media(_props);
+  }
+
+  /**
+   * Create a new media
+   * @param {Omit<Media, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} media_data - Media data
+   * @returns {Promise<Media>} Created media instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create(media_data) {
+    const { user_id, file_path, file_type, visibility = 'private', created_by_id = null } = media_data;
+    if (!user_id || !file_path || !file_type) {
+      throw new ValidationError('Missing required fields: user_id, file_path, file_type');
+    }
+    const query_str = `
+      INSERT INTO phase.media (user_id, file_path, file_type, visibility, created_by_id)
+      VALUES ($1, $2, $3, $4, $5) RETURNING *;
+    `;
+    const values = [user_id, file_path, file_type, visibility, created_by_id];
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new FailedToCreateError('Failed to create media');
+    return new Media(result);
+  }
+
+  /**
+   * Find media by file path
+   * @param {string} file_path - File path to search for
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @returns {Promise<Media|null>} Media instance or null
+   */
+  static async find_by_file_path(file_path, excludes = []) {
+    return await new Media().find_one({ file_path }, excludes);
+  }
+
+  /**
+   * Soft delete media
+   * @param {number|string} deleted_by_id - ID of user performing deletion
+   * @returns {Promise<Media>} Updated media instance
+   * @throws {ValidationError} If deleted_by_id is invalid
+   * @throws {NotFoundError} If record not found
+   */
+  async soft_delete(deleted_by_id) {
+    const deleted_by_id_int = parseInt(deleted_by_id, 10);
+    if (isNaN(deleted_by_id_int)) {
+      throw new ValidationError('deleted_by_id must be a valid integer');
+    }
+    return await this.update({
+      is_deleted: true,
+      deleted_at: new Date().toISOString(),
+      deleted_by_id: deleted_by_id_int
+    });
+  }
+}
+
+module.exports = Media;
\ No newline at end of file
diff --git a/src/models/message.model.js b/src/models/message.model.js
new file mode 100644 (file)
index 0000000..32ff965
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * @file Message model for phase.messages table
+ */
+
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model');
+
+/**
+ * @typedef {Object} Message
+ * @property {number} id - Message ID (primary key)
+ * @property {number} sender_id - Sender ID (foreign key to users)
+ * @property {number|null} group_id - Group ID (foreign key to message_groups)
+ * @property {number|null} recipient_id - Recipient ID (foreign key to users)
+ * @property {string} content - Message content
+ * @property {boolean} read - Whether the message has been read
+ * @property {Date|null} read_at - Timestamp when the message was read
+ * @property {Date} created_at - Creation timestamp
+ */
+
+/**
+ * Message model class
+ * @extends Model
+ */
+class Message extends Model {
+  /**
+   * Create a Message instance
+   * @param {Partial<Message>} [props] - Message properties
+   */
+  constructor(props) {
+    super(props);
+    this.table = 'phase.messages';
+    this.prepend = 'm';
+    this.default_columns = [
+      'id', 'sender_id', 'group_id', 'recipient_id', 'content',
+      'read', 'read_at', 'created_at'
+    ];
+    this.update_exclude_columns = ['id', 'created_at'];
+    this.base_query = `
+      SELECT m.id, m.sender_id, m.group_id, m.recipient_id, m.content,
+             m.read, m.read_at, m.created_at
+      FROM phase.messages m
+    `;
+    this.base_list_query = this.base_query;
+    this.default_order_by = 'ORDER BY m.created_at DESC';
+    this.instance = _props => new Message(_props);
+  }
+
+  /**
+   * Create a new message
+   * @param {Omit<Message, 'id'|'created_at'|'read'|'read_at'>} message_data - Message data
+   * @returns {Promise<Message>} Created message instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create(message_data) {
+    const { sender_id, group_id, recipient_id, content } = message_data;
+    if (!sender_id || !content || (group_id == null && recipient_id == null)) {
+      throw new ValidationError('Missing required fields: sender_id, content, and either group_id or recipient_id');
+    }
+    const query_str = `
+      INSERT INTO phase.messages (sender_id, group_id, recipient_id, content, read, read_at)
+      VALUES ($1, $2, $3, $4, FALSE, NULL) RETURNING *;
+    `;
+    const values = [sender_id, group_id, recipient_id, content];
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new FailedToCreateError('Failed to create message');
+    return new Message(result);
+  }
+
+  /**
+   * Find messages by group ID
+   * @param {number|string} group_id - Group ID
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<Message[]>} Array of messages
+   */
+  static async find_by_group_id(group_id, excludes = [], order_by = null, limit = 100, offset = 0) {
+    return await new Message().find_many({ group_id }, excludes, order_by, limit, offset);
+  }
+
+  /**
+   * Find messages by recipient ID
+   * @param {number|string} recipient_id - Recipient ID
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<Message[]>} Array of messages
+   */
+  static async find_by_recipient_id(recipient_id, excludes = [], order_by = null, limit = 100, offset = 0) {
+    return await new Message().find_many({ recipient_id }, excludes, order_by, limit, offset);
+  }
+
+  /**
+   * Mark a message as read
+   * @returns {Promise<Message>} Updated message instance
+   * @throws {NotFoundError} If record not found
+   */
+  async mark_as_read() {
+    return await this.update({
+      read: true,
+      read_at: new Date().toISOString()
+    });
+  }
+}
+
+module.exports = Message;
\ No newline at end of file
diff --git a/src/models/message_group.model.js b/src/models/message_group.model.js
new file mode 100644 (file)
index 0000000..2d715b3
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * @file Message group model for phase.message_groups table
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require('./model');
+
+/**
+ * @typedef {Object} MessageGroup
+ * @property {number} id - Message group ID (primary key)
+ * @property {string} name - Message group name
+ * @property {number} created_by_id - ID of user who created this message group
+ * @property {Date} created_at - Creation timestamp
+ */
+
+/**
+ * Message group model class
+ * @extends Model
+ */
+class MessageGroup extends Model {
+  /**
+   * Create a MessageGroup instance
+   * @param {Partial<MessageGroup>} [props] - Message group properties
+   */
+  constructor(props) {
+    super(props);
+    this.table = 'phase.message_groups';
+    this.prepend = 'mg';
+    this.default_columns = [
+      'id', 'name', 'created_by_id', 'created_at'
+    ];
+    this.update_exclude_columns = ['id', 'created_at'];
+    this.base_query = `
+      SELECT mg.id, mg.name, mg.created_by_id, mg.created_at
+      FROM phase.message_groups mg
+    `;
+    this.base_list_query = this.base_query;
+    this.default_order_by = 'ORDER BY mg.created_at DESC';
+    this.instance = _props => new MessageGroup(_props);
+  }
+
+  /**
+   * Create a new message group
+   * @param {Omit<MessageGroup, 'id'|'created_at'>} message_group_data - Message group data
+   * @returns {Promise<MessageGroup>} Created message group instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create(message_group_data) {
+    const { name, created_by_id } = message_group_data;
+    if (!name || !created_by_id) {
+      throw new ValidationError('Missing required fields: name, created_by_id');
+    }
+    const query_str = `
+      INSERT INTO phase.message_groups (name, created_by_id)
+      VALUES ($1, $2) RETURNING *;
+    `;
+    const values = [name, created_by_id];
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new FailedToCreateError('Failed to create message group');
+    return new MessageGroup(result);
+  }
+}
+
+module.exports = MessageGroup;
\ No newline at end of file
diff --git a/src/models/message_group_members.model.js b/src/models/message_group_members.model.js
new file mode 100644 (file)
index 0000000..9e7a182
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * @file Message group members model for phase.message_group_members table
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require('./model');
+
+/**
+ * @typedef {Object} MessageGroupMember
+ * @property {number} group_id - Message group ID (foreign key)
+ * @property {number} user_id - User ID (foreign key)
+ */
+
+/**
+ * Message group member model class
+ * @extends Model
+ */
+class MessageGroupMember extends Model {
+  /**
+   * Create a MessageGroupMember instance
+   * @param {Partial<MessageGroupMember>} [props] - Message group member properties
+   */
+  constructor(props) {
+    super(props);
+    this.table = 'phase.message_group_members';
+    this.prepend = 'mgm';
+    this.default_columns = ['group_id', 'user_id'];
+    this.update_exclude_columns = ['group_id', 'user_id'];
+    this.base_query = `
+      SELECT mgm.group_id, mgm.user_id
+      FROM phase.message_group_members mgm
+    `;
+    this.base_list_query = this.base_query;
+    this.default_order_by = 'ORDER BY mgm.group_id ASC';
+    this.instance = _props => new MessageGroupMember(_props);
+  }
+
+  /**
+   * Add a message group member relation
+   * @param {number|string} group_id - Message group ID
+   * @param {number|string} user_id - User ID
+   * @returns {Promise<MessageGroupMember>} Created relation instance
+   * @throws {ValidationError} If IDs are invalid
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async add_relation(group_id, user_id) {
+    const group_id_int = parseInt(group_id, 10);
+    const user_id_int = parseInt(user_id, 10);
+    if (isNaN(group_id_int) || isNaN(user_id_int)) {
+      throw new ValidationError('group_id and user_id must be valid integers');
+    }
+    const query_str = `
+      INSERT INTO phase.message_group_members (group_id, user_id)
+      VALUES ($1, $2) RETURNING *;
+    `;
+    const values = [group_id_int, user_id_int];
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new FailedToCreateError('Failed to add message group member relation');
+    return new MessageGroupMember(result);
+  }
+
+  /**
+   * Remove a message group member relation
+   * @param {number|string} user_id - User ID
+   * @param {number|string|undefined} [group_id=undefined] - Message group ID
+   * @returns {Promise<MessageGroupMember|null>} Deleted relation instance or null
+   * @throws {ValidationError} If IDs are invalid
+   */
+  static async remove_relation(user_id, group_id = undefined) {
+    const group_id_int = parseInt(group_id, 10);
+    const user_id_int = parseInt(user_id, 10);
+    if (isNaN(user_id_int)) {
+      throw new ValidationError('user_id must be a valid integer');
+    }
+    let query_str = `
+      DELETE FROM phase.message_group_members
+      WHERE user_id = $1 ${group_id_int ? 'AND group_id = $2' : ''} RETURNING *;
+    `;
+    const values = [user_id_int];
+    if (!isNaN(group_id_int)) values.push(group_id_int);
+    const result = await phsdb.query(query_str, values, { plain: !isNaN(group_id_int) });
+    return result ? new MessageGroupMember(result) : null;
+  }
+
+  /**
+   * Find message group member relation by IDs
+   * @param {number|string} group_id - Message group ID
+   * @param {number|string} user_id - User ID
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @returns {Promise<MessageGroupMember|null>} Relation instance or null
+   */
+  static async find_by_ids(group_id, user_id, excludes = []) {
+    return await new MessageGroupMember().find_one(
+      { group_id, user_id },
+      excludes
+    );
+  }
+
+  /**
+   * Find message group member relations by group ID
+   * @param {number|string} group_id - Message group ID
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<MessageGroupMember[]>} Array of relations
+   */
+  static async find_by_group_id(group_id, excludes = [], order_by = null, limit = 100, offset = 0) {
+    return await new MessageGroupMember().find_many(
+      { group_id },
+      excludes,
+      order_by,
+      limit,
+      offset
+    );
+  }
+}
+
+module.exports = MessageGroupMember;
\ No newline at end of file
diff --git a/src/models/post.model.js b/src/models/post.model.js
new file mode 100644 (file)
index 0000000..cba10d8
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * @file Post model for phase.posts table
+ */
+
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model');
+
+/**
+ * @typedef {Object} Post
+ * @property {number} id - Post ID (primary key)
+ * @property {number} user_id - User ID (foreign key)
+ * @property {string} title - Post title
+ * @property {string} content - Post content
+ * @property {string} post_type - Post type (e.g., 'blog', 'vlog')
+ * @property {string} visibility - Visibility (e.g., 'private', 'family', 'public')
+ * @property {number|null} created_by_id - ID of user who created this post
+ * @property {Date} created_at - Creation timestamp
+ * @property {boolean} is_deleted - Soft delete flag
+ * @property {number|null} deleted_by_id - ID of user who deleted this post
+ * @property {Date|null} deleted_at - Deletion timestamp
+ */
+
+/**
+ * Post model class
+ * @extends Model
+ */
+class Post extends Model {
+  /**
+   * Create a Post instance
+   * @param {Partial<Post>} [props] - Post properties
+   */
+  constructor(props) {
+    super(props);
+    this.table = 'phase.posts';
+    this.prepend = 'p';
+    this.default_columns = [
+      'id', 'user_id', 'title', 'content', 'post_type', 'visibility',
+      'created_by_id', 'created_at', 'is_deleted', 'deleted_by_id', 'deleted_at'
+    ];
+    this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id'];
+    this.base_query = `
+      SELECT p.id, p.user_id, p.title, p.content, p.post_type, p.visibility,
+             p.created_by_id, p.created_at, p.is_deleted, p.deleted_by_id, p.deleted_at
+      FROM phase.posts p
+      WHERE p.is_deleted = false
+    `;
+    this.base_list_query = `
+      SELECT p.id, p.user_id, p.title, p.content, p.post_type, p.visibility,
+             p.created_by_id, p.created_at
+      FROM phase.posts p
+      WHERE p.is_deleted = false
+    `;
+    this.default_order_by = 'ORDER BY p.created_at DESC';
+    this.instance = _props => new Post(_props);
+  }
+
+  /**
+   * Create a new post
+   * @param {Omit<Post, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} post_data - Post data
+   * @returns {Promise<Post>} Created post instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create(post_data) {
+    const { user_id, title, content, post_type, visibility = 'private', created_by_id = null } = post_data;
+    if (!user_id || !title || !content || !post_type) {
+      throw new ValidationError('Missing required fields: user_id, title, content, post_type');
+    }
+    const query_str = `
+      INSERT INTO phase.posts (user_id, title, content, post_type, visibility, created_by_id)
+      VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;
+    `;
+    const values = [user_id, title, content, post_type, visibility, created_by_id];
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new FailedToCreateError('Failed to create post');
+    return new Post(result);
+  }
+
+  /**
+   * Find post by title
+   * @param {string} title - Title to search for
+   * @param {string[]} [excludes] - Fields to exclude from result
+   * @returns {Promise<Post|null>} Post instance or null
+   */
+  static async find_by_title(title, excludes = []) {
+    return await new Post().find_one({ title }, excludes);
+  }
+
+  /**
+   * Soft delete post
+   * @param {number|string} deleted_by_id - ID of user performing deletion
+   * @returns {Promise<Post>} Updated post instance
+   * @throws {ValidationError} If deleted_by_id is invalid
+   * @throws {NotFoundError} If record not found
+   */
+  async soft_delete(deleted_by_id) {
+    const deleted_by_id_int = parseInt(deleted_by_id, 10);
+    if (isNaN(deleted_by_id_int)) {
+      throw new ValidationError('deleted_by_id must be a valid integer');
+    }
+    return await this.update({
+      is_deleted: true,
+      deleted_at: new Date().toISOString(),
+      deleted_by_id: deleted_by_id_int
+    });
+  }
+}
+
+module.exports = Post;
\ No newline at end of file
index 94edaeac283b7301e7c58f92ab75e48ba5f82f41..877db28efb816905e7b79d6fecda8d6182a6f38d 100644 (file)
@@ -2,7 +2,12 @@
  * @file User model for phase.users table
  */
 
-const { Model, ValidationError } = require( './model' );
+const db = require('../models');
+const bcrypt = require('bcrypt');
+const jwt = require('jsonwebtoken');
+const config = require('../config/default.json');
+const { Model, ValidationError } = require('./model');
+const createHttpError = require('http-errors');
 
 /**
  * @typedef {Object} User
@@ -28,12 +33,8 @@ const { Model, ValidationError } = require( './model' );
  * @extends Model
  */
 class User extends Model {
-  /**
-   * Create a User instance
-   * @param {Partial<User>} [props] - User properties
-   */
-  constructor( props ) {
-    super( props );
+  constructor(props) {
+    super(props);
     this.table = 'phase.users';
     this.prepend = 'u';
     this.default_columns = [
@@ -43,57 +44,29 @@ class User extends Model {
     ];
     this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id'];
     this.base_query = `
-        SELECT u.id,
-               u.email,
-               u.first_name,
-               u.middle_name,
-               u.last_name,
-               u.initials,
-               u.nickname,
-               u.created_by_id,
-               u.created_at,
-               u.is_deleted,
-               u.deleted_by_id,
-               u.deleted_at,
-               u.is_active,
-               u.deactivated_by_id,
-               u.deactivated_at
+        SELECT u.id, u.email, u.first_name, u.middle_name, u.last_name, u.initials, u.nickname,
+               u.created_by_id, u.created_at, u.is_deleted, u.deleted_by_id, u.deleted_at,
+               u.is_active, u.deactivated_by_id, u.deactivated_at
         FROM phase.users u
         WHERE u.is_deleted = false
     `;
     this.base_list_query = `
-        SELECT u.id,
-               u.email,
-               u.first_name,
-               u.middle_name,
-               u.last_name,
-               u.initials,
-               u.nickname,
-               u.created_by_id,
-               u.created_at,
-               u.is_active,
-               u.deactivated_by_id,
-               u.deactivated_at
+        SELECT u.id, u.email, u.first_name, u.middle_name, u.last_name, u.initials, u.nickname,
+               u.created_by_id, u.created_at, u.is_active, u.deactivated_by_id, u.deactivated_at
         FROM phase.users u
         WHERE u.is_deleted = false
     `;
     this.default_order_by = 'ORDER BY u.email ASC';
-    this.instance = _props => new User( _props );
+    this.instance = _props => new User(_props);
   }
 
-  /**
-   * Create a new user
-   * @param {Omit<User, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} user_data - User data
-   * @returns {Promise<User>} Created user instance
-   * @throws {ValidationError} If required fields are missing
-   */
-  static async create( user_data ) {
+  static async create(user_data) {
     const {
       email, first_name, middle_name = '', last_name, initials = null, nickname = null,
       created_by_id = null, is_active = true, deactivated_by_id = null, deactivated_at = null
     } = user_data;
     if (!email || !first_name || !last_name) {
-      throw new ValidationError( 'Missing required fields: email, first_name, last_name' );
+      throw new ValidationError('Missing required fields: email, first_name, last_name');
     }
     const query_str = `
         INSERT INTO phase.users (email, first_name, middle_name, last_name, initials, nickname, created_by_id,
@@ -105,148 +78,150 @@ class User extends Model {
       email, first_name, middle_name, last_name, initials, nickname,
       created_by_id, is_active, deactivated_by_id, deactivated_at
     ];
-    const result = await phsdb.query( query_str, values, { plain:true } );
-    if (!result) throw new ValidationError( 'Failed to create user' );
-    return new User( result );
-  };
+    const result = await phsdb.query(query_str, values, { plain: true });
+    if (!result) throw new ValidationError('Failed to create user');
+    return new User(result);
+  }
 
   async get_user_roles() {
     const query_str = `
-        SELECT r.*
+        SELECT r.name
         FROM phase.user_roles ur
-            inner join phase.roles r on r.id = ur.role_id
-        WHERE ur.user_id = $1 and r.is_deleted = false
+                 INNER JOIN phase.roles r ON r.id = ur.role_id
+        WHERE ur.user_id = $1 AND r.is_deleted = false
     `;
-    return await phsdb.query( query_str, [this.id], { plain:true } );
-  };
-
-  /**
-   * Find user by email
-   * @param {string} email - Email to search for
-   * @param {string[]} [excludes] - Fields to exclude from result
-   * @returns {Promise<User|null>} User instance or null
-   */
-  static async find_by_email( email, excludes = [] ) {
-    return await new User().find_one( { email }, excludes );
-  }
-
-  // noinspection JSUnusedGlobalSymbols
-  /**
-   * Find user by nickname
-   * @param {string} nickname - Nickname to search for
-   * @param {string[]} [excludes] - Fields to exclude from result
-   * @returns {Promise<User|null>} User instance or null
-   */
-  static async find_by_nickname( nickname, excludes = [] ) {
-    return await new User().find_one( { nickname }, excludes );
-  }
-
-  /**
-   * Find active users
-   * @param {string[]} [excludes] - Fields to exclude from result
-   * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-   * @param {number} [limit=100] - Maximum number of records
-   * @param {number} [offset=0] - Number of records to skip
-   * @returns {Promise<User[]>} Array of active users
-   */
-  static async find_active( excludes = [], order_by = null, limit = 100, offset = 0 ) {
-    return await new User().find_many( { is_active:true }, excludes, order_by, limit, offset );
-  };
-
-  // noinspection JSUnusedGlobalSymbols
-  /**
-   * Find deleted users
-   * @param {string[]} [excludes] - Fields to exclude from result
-   * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration
-   * @param {number} [limit=100] - Maximum number of records
-   * @param {number} [offset=0] - Number of records to skip
-   * @returns {Promise<User[]>} Array of deleted users
-   */
-  static async find_deleted( excludes = [], order_by = null, limit = 100, offset = 0 ) {
-    const query_str = this.prototype.base_list_query.replace( 'WHERE u.is_deleted = false', 'WHERE u.is_deleted = true' );
+    const result = await phsdb.query(query_str, [this.id]);
+    return result.map(r => r.name);
+  }
+
+  static async find_by_email(email, excludes = []) {
+    return await new User().find_one({ email }, excludes);
+  }
+
+  static async find_by_nickname(nickname, excludes = []) {
+    return await new User().find_one({ nickname }, excludes);
+  }
+
+  static async find_active(excludes = [], order_by = null, limit = 100, offset = 0) {
+    return await new User().find_many({ is_active: true }, excludes, order_by, limit, offset);
+  }
+
+  static async find_deleted(excludes = [], order_by = null, limit = 100, offset = 0) {
+    const query_str = this.prototype.base_list_query.replace('WHERE u.is_deleted = false', 'WHERE u.is_deleted = true');
     const instance = this.prototype.instance;
-    const { keys, values } = this.prototype.build_where( {}, this.prototype.default_columns );
-    values.push( limit, offset );
+    const { keys, values } = this.prototype.build_where({}, this.prototype.default_columns);
+    values.push(limit, offset);
     const results = await phsdb.query(
       `
-        ${ query_str }
-        ${ this.prototype.where_clause( keys, this.prototype.prepend ) }
-        ${ order_by ? `ORDER BY ${ order_by.name } ${ order_by.direction ?? 'asc' }` : this.prototype.default_order_by ?? '' }
-        LIMIT $${ values.length - 1 } OFFSET $${ values.length }
+        ${query_str}
+        ${this.prototype.where_clause(keys, this.prototype.prepend)}
+        ${order_by ? `ORDER BY ${order_by.name} ${order_by.direction ?? 'asc'}` : this.prototype.default_order_by ?? ''}
+        LIMIT $${values.length - 1} OFFSET $${values.length}
       `,
       values
     );
-    return results.map( result => {
-      const found = instance( result );
-      excludes?.forEach( e => delete found[e] );
+    return results.map(result => {
+      const found = instance(result);
+      excludes?.forEach(e => delete found[e]);
       return found;
-    } );
-  }
-
-  /**
-   * Deactivate user
-   * @param {number|string} deactivated_by_id - ID of user performing deactivation
-   * @returns {Promise<User>} Updated user instance
-   * @throws {ValidationError} If deactivated_by_id is invalid
-   */
-  async deactivate( deactivated_by_id ) {
-    const deactivated_by_id_int = parseInt( deactivated_by_id, 10 );
-    if (isNaN( deactivated_by_id_int )) {
-      throw new ValidationError( 'deactivated_by_id must be a valid integer' );
+    });
+  }
+
+  async deactivate(deactivated_by_id) {
+    const deactivated_by_id_int = parseInt(deactivated_by_id, 10);
+    if (isNaN(deactivated_by_id_int)) {
+      throw new ValidationError('deactivated_by_id must be a valid integer');
     }
-    return await this.update( {
-      is_active:false,
-      deactivated_at:new Date().toISOString(),
-      deactivated_by_id:deactivated_by_id_int
-    } );
+    return await this.update({
+      is_active: false,
+      deactivated_at: new Date().toISOString(),
+      deactivated_by_id: deactivated_by_id_int
+    });
   }
 
-  /**
-   * Reactivate user
-   * @returns {Promise<User>} Updated user instance
-   */
   async reactivate() {
-    return await this.update( {
-      is_active:true,
-      deactivated_at:null,
-      deactivated_by_id:null
-    } );
-  }
-
-  /**
-   * Soft delete user
-   * @param {number|string} deleted_by_id - ID of user performing deletion
-   * @returns {Promise<User>} Updated user instance
-   * @throws {ValidationError} If deleted_by_id is invalid
-   */
-  async soft_delete( deleted_by_id ) {
-    const deleted_by_id_int = parseInt( deleted_by_id, 10 );
-    if (isNaN( deleted_by_id_int )) {
-      throw new ValidationError( 'deleted_by_id must be a valid integer' );
+    return await this.update({
+      is_active: true,
+      deactivated_at: null,
+      deactivated_by_id: null
+    });
+  }
+
+  async comparePassword(password) {
+    const auth = await db.authentication.find_by_user_id(this.id);
+    return auth ? bcrypt.compareSync(password, auth.password) : false;
+  }
+
+  async createToken() {
+    const auth = await db.authentication.find_by_user_id(this.id);
+    const roles = await this.get_user_roles();
+    const tokenPayload = { id: this.id, email: this.email, roles };
+    const token = jwt.sign(tokenPayload, config.keys.secret, { expiresIn: '24h' });
+    const { exp } = jwt.decode(token);
+    const token_expiry = new Date(exp * 1000).toISOString();
+    await auth.update({
+      password_failures_since_last_success: 0,
+      password_verification_token: token,
+      password_verification_token_expiry: token_expiry
+    });
+    return token;
+  }
+
+  async failLogin() {
+    const auth = await db.authentication.find_by_user_id(this.id);
+    await auth.update({
+      password_failures_since_last_success: (auth.password_failures_since_last_success || 0) + 1,
+      password_verification_token: null,
+      password_verification_token_expiry: null,
+      last_password_failure: new Date().toISOString()
+    });
+    return true;
+  }
+
+  async lockAccount() {
+    const auth = await db.authentication.find_by_user_id(this.id);
+    await auth.update({
+      is_locked: true,
+      locked_date: new Date().toISOString()
+    });
+    return true;
+  }
+
+  async hashPassword(password) {
+    const auth = await db.authentication.find_by_user_id(this.id);
+    if (!auth) throw createHttpError(400, 'No authentication record found for this user.');
+    const salt = await bcrypt.genSalt(10);
+    const hash = await bcrypt.hash(password, salt);
+    await auth.update({
+      password_failures_since_last_success: 0,
+      password_changed_date: new Date().toISOString(),
+      password: hash,
+      password_salt: salt,
+      ms_password: false,
+      password_reset_token: null,
+      password_reset_expire_date: null
+    });
+    return { salt, hash };
+  }
+
+  async soft_delete(deleted_by_id) {
+    const deleted_by_id_int = parseInt(deleted_by_id, 10);
+    if (isNaN(deleted_by_id_int)) {
+      throw new ValidationError('deleted_by_id must be a valid integer');
     }
-    return await this.update( {
-      is_deleted:true,
-      deleted_at:new Date().toISOString(),
-      deleted_by_id:deleted_by_id_int
-    } );
+    return await this.update({
+      is_deleted: true,
+      deleted_at: new Date().toISOString(),
+      deleted_by_id: deleted_by_id_int
+    });
   }
 
-  /**
-   * Check if user is active
-   * @returns {boolean} True if user is active
-   */
   is_active() {
     return this.is_active === true;
   }
 
-  // noinspection JSUnusedGlobalSymbols
-  /**
-   * Get user data without sensitive information
-   * @returns {Omit<User, 'password'>} User data
-   */
   to_safe_json() {
     const { password, ...safe_data } = this.toJSON();
-    // noinspection JSValidateTypes
     return safe_data;
   }
 }
index 805421b9cb47355bb9a787569f776728cef5a55b..4449d5dd17b5b35065930ab4c6eb7dda37964196 100644 (file)
@@ -1,9 +1,8 @@
 const express = require('express');
 const router = express.Router();
-const { validateAuth } = require('../middleware/routeHelpers');
 const authController = require('../controllers/auth.controller');
 
-module.exports = (passport) => {
+module.exports = () => {
     router.post('/login', authController.login);
     return router;
 };
\ No newline at end of file
diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js
new file mode 100644 (file)
index 0000000..6d830e9
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * @file Media routes configuration
+ */
+
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const media_controller = require('../controllers/media.controller');
+
+/**
+ * Configure media routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with media routes
+ */
+module.exports = (passport) => {
+  router.post('/create', validateAuth(passport), media_controller.create);
+  router.get('/file_path/:file_path', validateAuth(passport), media_controller.find_by_file_path);
+  router.get('/:id', validateAuth(passport), media_controller.find_one);
+  router.get('/', validateAuth(passport), media_controller.find_many);
+  router.put('/:id', validateAuth(passport), media_controller.update);
+  router.put('/:id/soft_delete', validateAuth(passport), media_controller.soft_delete);
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/message.routes.js b/src/routes/message.routes.js
new file mode 100644 (file)
index 0000000..8d750f0
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * @file Message routes configuration
+ */
+
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const message_controller = require('../controllers/message.controller');
+
+/**
+ * Configure message routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with message routes
+ */
+module.exports = (passport) => {
+  router.post('/create', validateAuth(passport), message_controller.create);
+  router.get('/group/:group_id', validateAuth(passport), message_controller.find_by_group_id);
+  router.get('/recipient/:recipient_id', validateAuth(passport), message_controller.find_by_recipient_id);
+  router.get('/:id', validateAuth(passport), message_controller.find_one);
+  router.get('/', validateAuth(passport), message_controller.find_many);
+  router.put('/:id/mark_as_read', validateAuth(passport), message_controller.mark_as_read);
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/message_group.routes.js b/src/routes/message_group.routes.js
new file mode 100644 (file)
index 0000000..73135ab
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * @file Message group routes configuration
+ */
+
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const message_group_controller = require('../controllers/message_group.controller');
+
+/**
+ * Configure message group routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with message group routes
+ */
+module.exports = (passport) => {
+  router.post('/create', validateAuth(passport), message_group_controller.create);
+  router.get('/:id', validateAuth(passport), message_group_controller.find_one);
+  router.get('/', validateAuth(passport), message_group_controller.find_many);
+  router.put('/:id', validateAuth(passport), message_group_controller.update);
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/message_group_members.routes.js b/src/routes/message_group_members.routes.js
new file mode 100644 (file)
index 0000000..5739665
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * @file Message group members routes configuration
+ */
+
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const message_group_members_controller = require('../controllers/message_group_members.controller');
+
+/**
+ * Configure message group members routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with message group members routes
+ */
+module.exports = (passport) => {
+  router.post('/add', validateAuth(passport), message_group_members_controller.add_relation);
+  router.delete('/remove', validateAuth(passport), message_group_members_controller.remove_relation);
+  router.get('/ids/:group_id/:user_id', validateAuth(passport), message_group_members_controller.find_by_ids);
+  router.get('/group/:group_id', validateAuth(passport), message_group_members_controller.find_by_group_id);
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/post.routes.js b/src/routes/post.routes.js
new file mode 100644 (file)
index 0000000..0813c48
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * @file Post routes configuration
+ */
+
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const post_controller = require('../controllers/post.controller');
+
+/**
+ * Configure post routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with post routes
+ */
+module.exports = (passport) => {
+  router.post('/create', validateAuth(passport), post_controller.create);
+  router.get('/title/:title', validateAuth(passport), post_controller.find_by_title);
+  router.get('/:id', validateAuth(passport), post_controller.find_one);
+  router.get('/', validateAuth(passport), post_controller.find_many);
+  router.put('/:id', validateAuth(passport), post_controller.update);
+  router.put('/:id/soft_delete', validateAuth(passport), post_controller.soft_delete);
+  return router;
+};
\ No newline at end of file