]> PHS Git Server - phs-api.git/commitdiff
Adding Socket.io for messaging and getting things ready for a more robust UI.
authorcharleswrayjr <charleswrayjr@gmail.com>
Fri, 26 Sep 2025 22:13:22 +0000 (17:13 -0500)
committercharleswrayjr <charleswrayjr@gmail.com>
Fri, 26 Sep 2025 22:13:22 +0000 (17:13 -0500)
80 files changed:
.idea/dataSources.xml [changed mode: 0644->0755]
.idea/dictionaries/project.xml [changed mode: 0644->0755]
.idea/inspectionProfiles/Project_Default.xml [changed mode: 0644->0755]
.idea/sqldialects.xml [changed mode: 0644->0755]
.idea/zencoder-chat-index.xml [changed mode: 0644->0755]
app.js
package-lock.json
package.json
run-api.sh
src/config/secret_key_gen.js
src/controllers/address.controller.js [changed mode: 0644->0755]
src/controllers/auth.controller.js [changed mode: 0644->0755]
src/controllers/authentication.controller.js [changed mode: 0644->0755]
src/controllers/docker.controller.js [changed mode: 0644->0755]
src/controllers/file_users.controller.js [new file with mode: 0755]
src/controllers/files.controller.js [new file with mode: 0755]
src/controllers/git.controller.js
src/controllers/media.controller.js [deleted file]
src/controllers/message.controller.js [changed mode: 0644->0755]
src/controllers/message_files.controller.js [new file with mode: 0755]
src/controllers/message_group.controller.js [changed mode: 0644->0755]
src/controllers/message_group_members.controller.js [changed mode: 0644->0755]
src/controllers/phone_number.controller.js [changed mode: 0644->0755]
src/controllers/post.controller.js [changed mode: 0644->0755]
src/controllers/post_files.controller.js [changed mode: 0644->0755]
src/controllers/post_users.controller.js [changed mode: 0644->0755]
src/controllers/role.controller.js [changed mode: 0644->0755]
src/controllers/user.controller.js [changed mode: 0644->0755]
src/controllers/user_addresses.controller.js [changed mode: 0644->0755]
src/controllers/user_phone_numbers.controller.js [changed mode: 0644->0755]
src/controllers/user_roles.controller.js [changed mode: 0644->0755]
src/controllers/vpn.controller.js [changed mode: 0644->0755]
src/models/address.model.js [changed mode: 0644->0755]
src/models/authentication.model.js [changed mode: 0644->0755]
src/models/comment_reactions.model.js [new file with mode: 0755]
src/models/comments.model.js [new file with mode: 0755]
src/models/file.model.js [new file with mode: 0755]
src/models/file_users.model.js [new file with mode: 0755]
src/models/index.js [changed mode: 0644->0755]
src/models/media.model.js [deleted file]
src/models/message.model.js [changed mode: 0644->0755]
src/models/message_files.model.js [new file with mode: 0755]
src/models/message_group.model.js [changed mode: 0644->0755]
src/models/message_group_members.model.js [changed mode: 0644->0755]
src/models/message_reactions.model.js [new file with mode: 0755]
src/models/model.js
src/models/phone_number.model.js [changed mode: 0644->0755]
src/models/post.model.js [changed mode: 0644->0755]
src/models/post_files.model.js [changed mode: 0644->0755]
src/models/post_reactions.model.js [changed mode: 0644->0755]
src/models/post_users.model.js [changed mode: 0644->0755]
src/models/role.model.js [changed mode: 0644->0755]
src/models/user.model.js [changed mode: 0644->0755]
src/models/user_addresses.model.js [changed mode: 0644->0755]
src/models/user_phone_numbers.model.js [changed mode: 0644->0755]
src/models/user_roles.model.js [changed mode: 0644->0755]
src/phsdb.js
src/routes/address.routes.js [changed mode: 0644->0755]
src/routes/auth.routes.js [changed mode: 0644->0755]
src/routes/authentication.routes.js [changed mode: 0644->0755]
src/routes/docker.routes.js [changed mode: 0644->0755]
src/routes/file_users.routes.js [new file with mode: 0755]
src/routes/files.routes.js [new file with mode: 0755]
src/routes/index.js
src/routes/media.routes.js [deleted file]
src/routes/message.routes.js [changed mode: 0644->0755]
src/routes/message_files.routes.js [new file with mode: 0755]
src/routes/message_group.routes.js [changed mode: 0644->0755]
src/routes/message_group_members.routes.js [changed mode: 0644->0755]
src/routes/phone_number.routes.js [changed mode: 0644->0755]
src/routes/post.routes.js [changed mode: 0644->0755]
src/routes/post_files.routes.js [changed mode: 0644->0755]
src/routes/post_users.routes.js [changed mode: 0644->0755]
src/routes/role.routes.js [changed mode: 0644->0755]
src/routes/user.routes.js [changed mode: 0644->0755]
src/routes/user_addresses.routes.js [changed mode: 0644->0755]
src/routes/user_phone_numbers.routes.js [changed mode: 0644->0755]
src/routes/user_roles.routes.js [changed mode: 0644->0755]
src/routes/users.js [deleted file]
src/routes/vpn.routes.js [changed mode: 0644->0755]

old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 975388a..cf76012
@@ -1,7 +1,9 @@
 <component name="ProjectDictionaryState">
   <dictionary name="project">
     <words>
+      <w>appspecific</w>
       <w>certfile</w>
+      <w>dport</w>
       <w>easyrsa</w>
       <w>genkey</w>
       <w>ifconfig</w>
@@ -10,6 +12,7 @@
       <w>nopass</w>
       <w>ovpn</w>
       <w>passout</w>
+      <w>pgmigrations</w>
       <w>phasecustomsoft</w>
       <w>phsdb</w>
       <w>pkitool</w>
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/app.js b/app.js
index fa39c5a7b3a58ccfda6b12fd48881f999c7c964a..4346b14bf3acd27d9d9833c9e5952ad07088dbca 100755 (executable)
--- a/app.js
+++ b/app.js
@@ -1,36 +1,59 @@
-const createError = require('http-errors');
-const express = require('express');
+const createError = require( 'http-errors' );
+const express = require( 'express' );
 const compression = require( 'express-compression' );
 const bodyParser = require( 'body-parser' );
-const cookieParser = require('cookie-parser');
+const cookieParser = require( 'cookie-parser' );
 const log4js = require( './src/middleware/loggerMiddleWare' );
 const passport = require( 'passport' );
 let hookJWTStrategy = require( './src/middleware/passport' );
 const cors = require( 'cors' );
+const socketIo = require( 'socket.io' );
 const port = normalizePort( process.env.PORT || '3601' );
-const runOnce = require ( './src/middleware/custom-startup' );
+const runOnce = require( './src/middleware/custom-startup' );
 const Routes = require( './src/routes' );
 const phsdb = require( './src/phsdb' );
-const fs = require('fs');
-const path = require('path');
-const Docker = require('dockerode');
-const docker = new Docker({ socketPath: '/var/run/docker.sock' });
+const fs = require( 'fs' );
+const path = require( 'path' );
+const Docker = require( 'dockerode' );
+const db = require( './src/models' );
+const docker = new Docker( { socketPath:'/var/run/docker.sock' } );
 const sshConfig = {
   host:'192.168.1.62', port:22, username:'charles', privateKey:fs.readFileSync( '/home/node/.ssh/git-ui' ),
 };
 
-const buffer = require('buffer');
+const buffer = require( 'buffer' );
 global.Blob = buffer.Blob;
 
 const app = express();
-const http = require( 'http' ).createServer( app );
+const server = require( 'http' ).createServer( app );
+// noinspection JSValidateTypes
+const io = socketIo( server, {
+  cors:{
+    origin:[
+      'http://localhost:30007',
+      'http://phs-home:3000',
+      'http://localhost:23601',
+      'http://phs-api:3601',
+      'http://localhost:30008',
+      'http://phs-admin:3000',
+      'http://localhost:8101',
+      'https://admin.phasecustomsoft.com',
+      'https://app.phasecustomsoft.com',
+      'https://api.phasecustomsoft.com',
+      'https://www.phasecustomsoft.com'
+    ],
+    credentials:true,
+    methods:['GET', 'POST', 'PUT', 'DELETE'],
+    allowedHeaders:['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version']
+  }
+} );
 
-app.set('etag', false);
+app.set( 'etag', false );
 
-app.use((req, res, next) => {
-  res.set('Cache-Control', 'no-store');
+app.use( ( req, res, next ) => {
+  res.set( 'Cache-Control', 'no-store' );
   next();
-});
+} );
 
 global.phsdb = phsdb;
 global.fs = fs;
@@ -45,9 +68,9 @@ app.use( bodyParser.json( {
 } ) );
 app.use( bodyParser.urlencoded( {
   extended:true,
-  limit: '50mb'
+  limit:'50mb'
 } ) );
-app.use(cookieParser());
+app.use( cookieParser() );
 
 //Setting up log4js
 const logger = log4js.default;
@@ -70,11 +93,11 @@ global.logger = logger;
 // For automatic logging of all requests
 app.use( log4js.express );
 
-logger.info('PHS_ENV: ' + process.env.PHS_ENV );
-logger.info('NODE_ENV: ' + process.env.NODE_ENV );
-logger.info('Testing log levels: info, debug and error.  Only shows for PHS_ENV=DEV' );
-logger.info('Test log level: info' );   // Should show in all NODE_ENV settings
-logger.debug('Test log level: debug' ); // Should not show in PRODUCTION
+logger.info( 'PHS_ENV: ' + process.env.PHS_ENV );
+logger.info( 'NODE_ENV: ' + process.env.NODE_ENV );
+logger.info( 'Testing log levels: info, debug and error.  Only shows for PHS_ENV=DEV' );
+logger.info( 'Test log level: info' );   // Should show in all NODE_ENV settings
+logger.debug( 'Test log level: debug' ); // Should not show in PRODUCTION
 
 //Initialize passport
 app.use( passport.initialize() );
@@ -87,48 +110,29 @@ runOnce().catch( err => logger.error( err ) );
 app.use( compression() );
 app.use( express.json() );
 app.use( express.urlencoded( { extended:false } ) );
-app.use(cookieParser());
+app.use( cookieParser() );
 
 // 2FA Cors:
-app.use(cors({
-  exposedHeaders: ['LastFetchDateTime', 'Authentication'],
-  origin: [
-    // Home
-    'http://localhost:30008', // local dev
-    'http://localhost:23601',
-    'http://phs-home:3000', // import / export jobs
-    'https://phs-home.phasecustomsoft.com',
-    'https://phs-admin.phasecustomsoft.com',
-    'https://www.phasecustomsoft.com',
-    // Customer (local dev)
-    'http://localhost:30006', // local dev
-    'http://rt2-customer:3000', // import / export jobs
-    'https://customer-f.phasecustomsoft.com',
-    'https://customer-t.phasecustomsoft.com',
-    'https://customer-demo.phasecustomsoft.com',
-    'https://customer.phasecustomsoft.com',
-    // Admin (local dev)
-    'http://localhost:8100',
+app.use( cors( {
+  exposedHeaders:['LastFetchDateTime', 'Authentication'],
+  origin:[
     'http://localhost:30007',
-    'https://admin-t.phasecustomsoft.com',
-    'https://admin-f.phasecustomsoft.com',
-    'https://admin-demo.phasecustomsoft.com',
-    'https://admin.phasecustomsoft.com',
-    // App (local dev)
+    'http://phs-home:3000',
+    'http://localhost:23601',
+    'http://phs-api:3601',
+    'http://localhost:30008',
+    'http://phs-admin:3000',
     'http://localhost:8101',
-    'https://app-t.phasecustomsoft.com',
-    'https://app-f.phasecustomsoft.com',
-    'https://app-demo.phasecustomsoft.com',
+    'https://admin.phasecustomsoft.com',
     'https://app.phasecustomsoft.com',
-    // Public website
-    'https://www-t.phasecustomsoft.com',
+    'https://api.phasecustomsoft.com',
     'https://www.phasecustomsoft.com'
   ],
-  credentials: true,
-  methods: ['GET', 'POST', 'PUT', 'DELETE'],
-  allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version'],
-  optionsSuccessResponds: true,
-}));
+  credentials:true,
+  methods:['GET', 'POST', 'PUT', 'DELETE'],
+  allowedHeaders:['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version'],
+  optionsSuccessResponds:true
+} ) );
 
 //Send the current server date/time for each request
 app.use( function ( req, res, next ) {
@@ -136,29 +140,78 @@ app.use( function ( req, res, next ) {
   next();
 } );
 
+// Socket.io for real-time chat and reactions
+io.on( 'connection', ( socket ) => {
+  const userId = socket.handshake.query.userId;
+  logger.info( `Socket.io connection established with userId: ${ userId || 'undefined' }` );
+  if (userId) {
+    socket.join( userId );
+  } else {
+    logger.warn( 'Socket.io connection with missing userId' );
+  }
+  socket.on( 'chatMessage', async ( { content, recipientId, groupId, fileIds } ) => {
+    try {
+      const message_data = {
+        sender_id:userId,
+        group_id:groupId ? parseInt( groupId ) : null,
+        recipient_id:recipientId ? parseInt( recipientId ) : null,
+        content,
+        file_ids:fileIds || []
+      };
+      const message = await db.message.create( message_data );
+      if (groupId) {
+        const members = await phsdb.query( 'SELECT user_id FROM phase.message_group_members WHERE group_id = $1', [groupId] );
+        members.forEach( m => io.to( m.user_id.toString() ).emit( 'newMessage', message ) );
+      } else if (recipientId) {
+        io.to( recipientId.toString() ).emit( 'newMessage', message );
+        if (userId) io.to( userId.toString() ).emit( 'newMessage', message );
+      }
+    } catch (err) {
+      logger.error( `Socket chatMessage error: ${ err.message }` );
+      socket.emit( 'error', { error:err.message } );
+    }
+  } );
+  socket.on( 'messageReaction', async ( { messageId, reaction } ) => {
+    try {
+      const reaction_data = await db.message_reactions.add( messageId, userId, reaction );
+      const message = await db.message.find_one( { id:parseInt( messageId ) } );
+      if (message.group_id) {
+        const members = await phsdb.query( 'SELECT user_id FROM phase.message_group_members WHERE group_id = $1', [message.group_id] );
+        members.forEach( m => io.to( m.user_id.toString() ).emit( 'newReaction', { messageId, reaction_data } ) );
+      } else if (message.recipient_id) {
+        io.to( message.recipient_id.toString() ).emit( 'newReaction', { messageId, reaction_data } );
+        if (userId) io.to( userId.toString() ).emit( 'newReaction', { messageId, reaction_data } );
+      }
+    } catch (err) {
+      logger.error( `Socket messageReaction error: ${ err.message }` );
+      socket.emit( 'error', { error:err.message } );
+    }
+  } );
+} );
+
 app.use( '/', Routes.APIRoutes( passport ) );
 app.set( 'port', port );
 //Create HTTP Server for App
 // noinspection JSCheckFunctionSignatures
-http.listen( port );
-http.on( 'error', onError );
-http.on( 'listening', onListening );
+server.listen( port );
+server.on( 'error', onError );
+server.on( 'listening', onListening );
 
 
-process.on('uncaughtException', function(err) {
+process.on( 'uncaughtException', function ( err ) {
   logger.error( '*************uncaughtException******' );
   logger.error( err );
-});
+} );
 
-process.on('unhandledRejection', (err) => {
+process.on( 'unhandledRejection', ( err ) => {
   logger.error( '*************unhandledRejection******' );
-  logger.error(err.name, err.message);
-});
+  logger.error( err.name, err.message );
+} );
 
 
 // catch 404 and forward to the error handler
 app.use( function ( req, res, next ) {
-  if (req.url === '/.well-known/appspecific/com.chrome.devtools.json' ) next();
+  if (req.url === '/.well-known/appspecific/com.chrome.devtools.json') next();
   else {
     next( createError( 404 ) );
   }
@@ -212,7 +265,7 @@ function onError( error ) {
  */
 
 function onListening() {
-  const addr = http.address();
+  const addr = server.address();
   const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
   logger.info( 'Listening on ' + bind );
 }
@@ -227,7 +280,7 @@ const errorHandler = ( err, req, res, next ) => {
     res.locals.error = req.app.get( 'env' ) === 'DEVELOPMENT' ? err : {};
     // render the error page
     res.status( err.status || 500 );
-    res.send(err);
+    res.send( err );
   } else next();
 };
 
index a0b3a3bfba80d94a0c5519980d63ac73a285602f..c7ddfdab4298ed78ec67316dabb81754c9a7ebf8 100755 (executable)
@@ -26,6 +26,7 @@
         "pg": "^8.16.3",
         "pg-promise": "^11.15.0",
         "pug": "3.0.3",
+        "socket.io": "^4.8.1",
         "ssh2": "^1.17.0",
         "swagger-ui-express": "^5.0.1"
       },
         "@sinonjs/commons": "^3.0.1"
       }
     },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
+    },
     "node_modules/@tybys/wasm-util": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
         "@babel/types": "^7.28.2"
       }
     },
+    "node_modules/@types/cors": {
+      "version": "2.8.19",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/istanbul-lib-coverage": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
         }
       ]
     },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
     "node_modules/basic-auth": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
         "once": "^1.4.0"
       }
     },
+    "node_modules/engine.io": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+      "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
+      "dependencies": {
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.7.2",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.17.1"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/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/engine.io/node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/error-ex": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/socket.io": {
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+      "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.3.2",
+        "engine.io": "~6.6.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.5.5",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+      "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+      "dependencies": {
+        "debug": "~4.3.4",
+        "ws": "~8.17.1"
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-adapter/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/socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser/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/socket.io/node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/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/socket.io/node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
+    "node_modules/ws": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
index b279d6ee4077ce97c073c23b5729193d46e08d18..93a7e4c3adfe9eb0e06f030a59b6a309f9443c8b 100755 (executable)
@@ -24,6 +24,7 @@
     "pg": "^8.16.3",
     "pg-promise": "^11.15.0",
     "pug": "3.0.3",
+    "socket.io": "^4.8.1",
     "ssh2": "^1.17.0",
     "swagger-ui-express": "^5.0.1"
   },
index 641e66cc2e6aa69e0f112510fde3c30b5e63f77b..a969a793c400cfc6ab54ac8400aa4c7906b298a0 100755 (executable)
@@ -10,11 +10,11 @@ if [ "$env" == "TEST" ]; then
    npm install; npx pm2 start api.config.cjs --no-daemon --env TEST
 elif [ "$env" == "PRODUCTION" ]; then
    echo "PHS API Production Stack"
-#   npm install; npm run start-debug
-   npm install;
-   npx babel src --out-dir src --extensions .jsx;
-   cd /usr/src/app
-   npx pm2 start api.config.cjs --no-daemon --env PRODUCTION
+   npm install; npm run start-debug
+#   npm install;
+#   npx babel src --out-dir src --extensions .jsx;
+#   cd /usr/src/app
+#   npx pm2 start api.config.cjs --no-daemon --env PRODUCTION
 else
    echo "PHS API Dev Stack"
    npm install; npm run start-debug
index 998ed7c7963a27b96c2ab35018391f1d2e51a2a4..de206ab90c47fcc294fc61650c8f45157e719958 100755 (executable)
@@ -2,7 +2,7 @@ const crypto = require('crypto');
 const jwt = require('jsonwebtoken');
 const config = require( '../config/default.json' );
 
-// Generate (16 bytes, 128 bits) secret key. Copy output to to the proper key in default.json.
+// Generate (16 bytes, 128 bits) secret key. Copy output to the proper key in default.json.
 const secretKey = crypto.randomBytes(16).toString('hex');
 logger.debug('key: ' + secretKey);
 
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 8549cb1..68b39b9
@@ -4,25 +4,24 @@
 
 const db = require( '../models' );
 const createError = require( 'http-errors' );
-const jwt = require('jsonwebtoken');
-const config = require('../config/default.json');
-const phs_env = process.env.PHS_ENV;
+const jwt = require( 'jsonwebtoken' );
+const config = require( '../config/default.json' );
 
 const updateUserAndReturn = async ( validUser, res ) => {
   const token = await validUser.createToken();
-  await setCookie('phase_request_token', { user_id: validUser.id, request_token: token }, res);
+  await setCookie( 'phase_request_token', { user_id:validUser.id, request_token:token }, res );
   res.status( 200 ).send( { success:true, user:validUser, token } );
 };
 
-const setCookie = (cookieKey, payload, res) => {
-  const token = jwt.sign(payload, config.keys.secret2fa, { algorithm: 'HS256', expiresIn: '1y' });
-  res.cookie(cookieKey, token, {
-    maxAge: 31536000000,
-    httpOnly: true,
-    secure: (phs_env !== 'DEV'),
-    sameSite: 'lax',
-    domain: '.phasecustomsoft.com'
-  }); // expire in 1 year: 31536000000
+const setCookie = ( cookieKey, payload, res ) => {
+  const token = jwt.sign( payload, config.keys.secret2fa, { algorithm:'HS256', expiresIn:'1y' } );
+  res.cookie( cookieKey, token, {
+    maxAge:24 * 60 * 60 * 1000, // 1 day
+    httpOnly:true,
+    secure:true,
+    sameSite:'lax',
+    domain:'.phasecustomsoft.com'
+  } ); // expire in 1 year: 31536000000
 };
 
 module.exports = {
@@ -47,4 +46,29 @@ module.exports = {
       next( e );
     }
   },
+
+  /**
+   * Logout a user by clearing the JWT cookie
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async logout( req, res, next ) {
+    try {
+      // Clear the phase_request_token cookie
+      res.clearCookie( 'phase_request_token', {
+        maxAge:24 * 60 * 60 * 1000, // 1 day
+        httpOnly:true,
+        secure:true,
+        domain:'.phasecustomsoft.com',
+        sameSite:'lax'
+      } );
+      logger.info( `User logged out: ${ req.user?.id || 'unknown' }` );
+      res.json( { message:'Logged out successfully' } );
+    } catch (error) {
+      logger.error( `Logout error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  }
 };
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/src/controllers/file_users.controller.js b/src/controllers/file_users.controller.js
new file mode 100755 (executable)
index 0000000..479b720
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * @file File users controller for handling file-users relation API requests
+ * @module FileUsersController
+ */
+
+const db = require( '../models' );
+const createError = require( 'http-errors' );
+
+/**
+ * File users controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Add a file-user 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 { file_id, user_id } = req.body;
+      const file = await db.file.find_one( { id: parseInt( file_id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to add relation to this file' ) );
+      }
+      const user = await db.user.find_one( { id: parseInt( user_id ) } );
+      if ( !user ) return next( createError( 404, 'User not found' ) );
+      const relation = await db.file_users.add_relation( file_id, user_id );
+      res.json( relation );
+    } catch ( error ) {
+      logger.error( `Add file-user relation error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Remove a file-user 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 { file_id, user_id } = req.params;
+      const file = await db.file.find_one( { id: parseInt( file_id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to remove relation from this file' ) );
+      }
+      const user = await db.user.find_one( { id: parseInt( user_id ) } );
+      if ( !user ) return next( createError( 404, 'User not found' ) );
+      const relation = await db.file_users.remove_relation( file_id, user_id );
+      res.json( relation || { message: 'Relation removed' } );
+    } catch ( error ) {
+      logger.error( `Remove file-user relation error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Find file-user relations by file 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_file_id( req, res, next ) {
+    try {
+      const { file_id } = req.params;
+      const { limit = '100', offset = '0' } = req.query;
+      const relations = await db.file_users.find_by_file_id( parseInt( file_id ), [], null, parseInt( limit ), parseInt( offset ) );
+      res.json( relations );
+    } catch ( error ) {
+      logger.error( `Find file-user relations by file ID error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  }
+};
\ No newline at end of file
diff --git a/src/controllers/files.controller.js b/src/controllers/files.controller.js
new file mode 100755 (executable)
index 0000000..d6b5cfa
--- /dev/null
@@ -0,0 +1,191 @@
+/**
+ * @file Files controller for handling file-related API requests
+ * @module FilesController
+ */
+
+const db = require( '../models' );
+const createError = require( 'http-errors' );
+const { upload } = require( '../middleware/multer' );
+
+/**
+ * Files controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Multer middleware for file uploads
+   * @type {Function||any}
+   */
+  uploadFile: upload.array( 'files', 10 ),
+
+  /**
+   * Create a new file
+   * @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 { visibility, specific_users } = req.body;
+      const files = req.files || [];
+      const results = [];
+      for ( const file of files ) {
+        const file_data = {
+          user_id: req.user.id,
+          file_path: `/uploads/${ file.filename }`,
+          file_type: file.mimetype.startsWith( 'image' ) ? 'image' : 'video',
+          visibility: visibility || 'family',
+          specific_users: specific_users ? JSON.parse( specific_users ) : [],
+          created_by_id: req.user.id
+        };
+        const uploaded_file = await db.file.create( file_data );
+        results.push( uploaded_file );
+      }
+      res.json( results.length === 1 ? results[ 0 ] : results );
+    } catch ( error ) {
+      logger.error( `Create file error: ${ error.message }` );
+      next( createError( error.status || 409, error.message ) );
+    }
+  },
+
+  /**
+   * Find file 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 file = await db.file.find_by_file_path( file_path );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      res.json( file );
+    } catch ( error ) {
+      logger.error( `Find file by file path error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  },
+
+  /**
+   * Find one file 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 file = await db.file.find_one( { id: parseInt( id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      res.json( file );
+    } catch ( error ) {
+      logger.error( `Find file error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  },
+
+  /**
+   * Find many files
+   * @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;
+      where.is_deleted = false;
+      if ( req.user ) {
+        where[ '$or' ] = [
+          { visibility: 'family' },
+          { user_id: req.user.id },
+          { specific_users: { $contains: [ req.user.id ] } }
+        ];
+      }
+      const files = await db.file.find_many( where, [], null, parseInt( limit ), parseInt( offset ) );
+      res.json( files );
+    } catch ( error ) {
+      logger.error( `Find many files error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  },
+
+  /**
+   * Update a file
+   * @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 file_data = req.body;
+      const file = await db.file.instance().find_one( { id: parseInt( id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to update this file' ) );
+      }
+      const updated_file = await file.update( file_data );
+      res.json( updated_file );
+    } catch ( error ) {
+      logger.error( `Update file error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Soft delete a file
+   * @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 file = await db.file.instance().find_one( { id: parseInt( id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to delete this file' ) );
+      }
+      const deleted_file = await file.soft_delete( deleted_by_id || req.user.id );
+      res.json( deleted_file );
+    } catch ( error ) {
+      logger.error( `Soft delete file error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Serve a file
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async get_file( req, res, next ) {
+    try {
+      const { id } = req.params;
+      const file = await db.file.find_one( { id: parseInt( id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      if ( file.visibility === 'personal' && file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to access this file' ) );
+      }
+      if ( file.visibility === 'specific' ) {
+        const allowed_users = await db.file_users.find_by_file_id( file.id );
+        if ( !allowed_users.some( u => u.user_id === req.user.id ) && !req.user.roles.includes( 'admin' ) ) {
+          return next( createError( 403, 'Unauthorized to access this file' ) );
+        }
+      }
+      res.sendFile( file.file_path, { root: '.' } );
+    } catch ( error ) {
+      logger.error( `Get file error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  }
+};
\ No newline at end of file
index d6c2619011340cbd452d25b02e191c43e939142f..4a54936a59811e04c9c17d02cd670f27d0b501a8 100755 (executable)
@@ -17,7 +17,8 @@ module.exports = {
       conn.exec( commands.join( ' && ' ), ( err, stream ) => {
         if (err) {
           conn.end();
-          return res.status( 500 ).json( { error:'SSH command failed' } );
+          const error = new createError( 500, `SSH command failed` );
+          next( error );
         }
         let output = '';
         stream.on( 'data', ( data ) => (output += data) );
@@ -27,12 +28,14 @@ module.exports = {
           if (code === 0) {
             res.json( { message:`Repository ${ name }.git created successfully` } );
           } else {
-            res.status( 500 ).json( { error:`Command failed: ${ output }` } );
+            const error = new createError( 500, `Command failed: ${ output }` );
+            next( error );
           }
         } );
       } );
     } ).on( 'error', ( err ) => {
-      res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } );
+      const error = new createError( 500, `SSH connection failed: ${ err.message }` );
+      next( error );
     } ).connect( sshConfig );
   }, getRepos:async ( req, res, next ) => {
     const conn = new Client();
@@ -41,7 +44,8 @@ module.exports = {
       conn.exec( command, ( err, stream ) => {
         if (err) {
           conn.end();
-          return res.status( 500 ).json( { error:'SSH command failed' } );
+          const error = new createError( 500, `SSH command failed` );
+          next( error );
         }
         let output = '';
         stream.on( 'data', ( data ) => (output += data) );
@@ -54,17 +58,20 @@ module.exports = {
             }) );
             res.json( { repos } );
           } else {
-            res.status( 500 ).json( { error:`Command failed: ${ output }` } );
+            const error = new createError( 500, `Command failed: ${ output }` );
+            next( error );
           }
         } );
       } );
     } ).on( 'error', ( err ) => {
-      res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } );
+      const error = new createError( 500, `SSH connection failed: ${ err.message }` );
+      next( error );
     } ).connect( sshConfig );
   }, deleteRepo:async ( req, res, next ) => {
     const { name } = req.params;
     if (!name.match( /^[a-zA-Z0-9_-]+$/ )) {
-      return res.status( 400 ).json( { error:'Invalid repository name' } );
+      const error = new createError( 400, 'Invalid repository name' );
+      next( error );
     }
 
     const conn = new Client();
@@ -73,7 +80,8 @@ module.exports = {
       conn.exec( command, ( err, stream ) => {
         if (err) {
           conn.end();
-          return res.status( 500 ).json( { error:'SSH command failed' } );
+          const error = new createError( 500, `SSH command failed` );
+          next( error );
         }
         let output = '';
         stream.on( 'data', ( data ) => (output += data) );
@@ -83,20 +91,24 @@ module.exports = {
           if (code === 0) {
             res.json( { message:`Repository ${ name } deleted successfully` } );
           } else {
-            res.status( 500 ).json( { error:`Command failed: ${ output }` } );
+            const error = new createError( 500, `Command failed: ${ output }` );
+            next( error );
           }
         } );
       } );
     } ).on( 'error', ( err ) => {
-      res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } );
+      const error = new createError( 500, `SSH connection failed: ${ err.message }` );
+      next( error );
     } ).connect( sshConfig );
   }, cloneRepo:async ( req, res, next ) => {
     const { repoName, deployPath, user } = req.body;
     if (!repoName || !deployPath || !user) {
-      return res.status( 400 ).json( { error:'Repository name, deployment path, and user are required' } );
+      const error = new createError( 400, 'Repository name, deployment path, and user are required' );
+      next( error );
     }
     if (!repoName.match( /^[a-zA-Z0-9_-]+$/ )) {
-      return res.status( 400 ).json( { error:'Invalid repository name' } );
+      const error = new createError( 400, 'Invalid repository name' );
+      next( error );
     }
 
     const conn = new Client();
@@ -105,7 +117,8 @@ module.exports = {
       conn.exec( commands.join( ' && ' ), ( err, stream ) => {
         if (err) {
           conn.end();
-          return res.status( 500 ).json( { error:'SSH command failed' } );
+          const error = new createError( 500, `SSH command failed` );
+          next( error );
         }
         let output = '';
         stream.on( 'data', ( data ) => (output += data) );
@@ -115,12 +128,14 @@ module.exports = {
           if (code === 0) {
             res.json( { message:`Repository ${ repoName } cloned to ${ deployPath }` } );
           } else {
-            res.status( 500 ).json( { error:`Command failed: ${ output }` } );
+            const error = new createError( 500, `Command failed: ${ output }` );
+            next( error );
           }
         } );
       } );
     } ).on( 'error', ( err ) => {
-      res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } );
+      const error = new createError( 500, `SSH connection failed: ${ err.message }` );
+      next( error );
     } ).connect( sshConfig );
   },
 };
\ No newline at end of file
diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js
deleted file mode 100644 (file)
index d4fdde2..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * @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
old mode 100644 (file)
new mode 100755 (executable)
index b72e7ce..d81026c
@@ -1,10 +1,10 @@
 /**
  * @file Message controller for handling message-related API requests
+ * @module MessageController
  */
 
-const db = require('../models');
-const createError = require('http-errors');
-const logger = global.logger;
+const db = require( '../models' );
+const createError = require( 'http-errors' );
 
 /**
  * Message controller
@@ -18,14 +18,33 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async create(req, res, next) {
+  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));
+      const { sender_id, group_id, recipient_id, content, files = [] } = req.body;
+      const message_data = {
+        sender_id: sender_id || req.user.id,
+        group_id: group_id ? parseInt( group_id ) : null,
+        recipient_id: recipient_id ? parseInt( recipient_id ) : null,
+        content,
+        file_ids: []
+      };
+      for ( const file of files ) {
+        const file_data = {
+          user_id: req.user.id,
+          file_path: file.path,
+          file_type: file.mimetype.startsWith( 'image' ) ? 'image' : 'video',
+          visibility: group_id ? 'family' : recipient_id ? 'specific' : 'family',
+          specific_users: recipient_id ? [ recipient_id ] : [],
+          created_by_id: req.user.id
+        };
+        const uploaded_file = await db.file.create( file_data );
+        message_data.file_ids.push( uploaded_file.id );
+      }
+      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 ) );
     }
   },
 
@@ -36,15 +55,15 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_by_group_id(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -55,15 +74,15 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_by_recipient_id(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -74,16 +93,15 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_one(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -94,14 +112,27 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_many(req, res, next) {
+  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));
+      const { limit = '100', offset = '0', ...where } = req.query;
+      if ( req.user ) {
+        where[ '$or' ] = [
+          {
+            group_id: {
+              $in: `SELECT group_id
+                    FROM phase.message_group_members
+                    WHERE user_id = ${ req.user.id }`
+            }
+          },
+          { recipient_id: req.user.id },
+          { sender_id: req.user.id }
+        ];
+      }
+      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 ) );
     }
   },
 
@@ -112,16 +143,38 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async mark_as_read(req, res, next) {
+  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 message = await db.message.instance().find_one( { id: parseInt( id ) } );
+      if ( !message ) return next( createError( 404, 'Message not found' ) );
+      if ( message.recipient_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to mark this message as read' ) );
+      }
       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));
+      res.json( updated_message );
+    } catch ( error ) {
+      logger.error( `Mark message as read error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Add a reaction to a message
+   * @param {Object} req - Express request object
+   * @param {Object} res - Express response object
+   * @param {Function} next - Express next middleware function
+   * @returns {Promise<void>}
+   */
+  async add_reaction( req, res, next ) {
+    try {
+      const { messageId } = req.params;
+      const { reaction } = req.body;
+      const reaction_data = await db.message_reactions.add( messageId, req.user.id, reaction );
+      res.json( reaction_data );
+    } catch ( error ) {
+      logger.error( `Add message reaction error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
     }
   }
 };
\ No newline at end of file
diff --git a/src/controllers/message_files.controller.js b/src/controllers/message_files.controller.js
new file mode 100755 (executable)
index 0000000..f0d7fc7
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * @file Message files controller for handling message-files relation API requests
+ * @module MessageFilesController
+ */
+
+const db = require( '../models' );
+const createError = require( 'http-errors' );
+
+/**
+ * Message files controller
+ * @type {Object}
+ */
+module.exports = {
+  /**
+   * Add a message-file 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 { message_id, file_id } = req.body;
+      const message = await db.message.find_one( { id: parseInt( message_id ) } );
+      if ( !message ) return next( createError( 404, 'Message not found' ) );
+      if ( message.sender_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to add relation to this message' ) );
+      }
+      const file = await db.file.find_one( { id: parseInt( file_id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      const relation = await db.message_files.add_relation( message_id, file_id );
+      res.json( relation );
+    } catch ( error ) {
+      logger.error( `Add message-file relation error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Remove a message-file 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 { message_id, file_id } = req.params;
+      const message = await db.message.find_one( { id: parseInt( message_id ) } );
+      if ( !message ) return next( createError( 404, 'Message not found' ) );
+      if ( message.sender_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+        return next( createError( 403, 'Unauthorized to remove relation from this message' ) );
+      }
+      const file = await db.file.find_one( { id: parseInt( file_id ) } );
+      if ( !file ) return next( createError( 404, 'File not found' ) );
+      const relation = await db.message_files.remove_relation( message_id, file_id );
+      res.json( relation || { message: 'Relation removed' } );
+    } catch ( error ) {
+      logger.error( `Remove message-file relation error: ${ error.message }` );
+      next( createError( error.status || 400, error.message ) );
+    }
+  },
+
+  /**
+   * Find message-file relations by message 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_message_id( req, res, next ) {
+    try {
+      const { message_id } = req.params;
+      const { limit = '100', offset = '0' } = req.query;
+      const relations = await db.message_files.find_by_message_id( parseInt( message_id ), [], null, parseInt( limit ), parseInt( offset ) );
+      res.json( relations );
+    } catch ( error ) {
+      logger.error( `Find message-file relations by message ID error: ${ error.message }` );
+      next( createError( error.status || 500, error.message ) );
+    }
+  }
+};
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
index 55909d3..5b5d4b8
@@ -2,9 +2,8 @@
  * @file Message group controller for handling message group-related API requests
  */
 
-const db = require('../models');
-const createError = require('http-errors');
-const logger = global.logger;
+const db = require( '../models' );
+const createError = require( 'http-errors' );
 
 /**
  * Message group controller
@@ -18,14 +17,14 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async create(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -36,15 +35,15 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_one(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -55,14 +54,14 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async find_many(req, res, next) {
+  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));
+      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 ) );
     }
   },
 
@@ -73,17 +72,17 @@ module.exports = {
    * @param {Function} next - Express next middleware function
    * @returns {Promise<void>}
    */
-  async update(req, res, next) {
+  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));
+      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
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index cfa9418..dc62f79
@@ -59,7 +59,7 @@ module.exports = {
       const { limit = '100', offset = '0', ...where } = req.query;
       const users = await db.user.find_many( where, [], null, parseInt( limit ), parseInt( offset ) );
       if (!users.length) return next( createError( 404, 'No users found' ) );
-      res.status( 200 ).send( users.map( u => new (require( '../models/user.model' )( u ).toJSON()) ) );
+      res.status( 200 ).send( users );
     } catch (error) {
       logger.error( `Index users error: ${ error.message }` );
       next( createError( error.status || 500, error.message ) );
@@ -190,7 +190,7 @@ module.exports = {
   async get_current_user( req, res, next ) {
     try {
       const { id } = req.user;
-      const user = await db.user.instance().find_one( { id: id } );
+      const user = await db.user.instance().find_one( { id:id } );
       if (!user) return next( createError( 404, 'User not found' ) );
       res.status( 200 ).send( user );
     } catch (error) {
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 66eb22f..8de62f6
@@ -5,7 +5,7 @@
 const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' );
 
 /**
- * @typedef {Object} Authentication
+ * @typedef {Object} Authentication_props
  * @property {number} id - Authentication ID (primary key)
  * @property {number} user_id - User ID (foreign key)
  * @property {string} password - Hashed password
@@ -25,12 +25,32 @@ const { Model, NotFoundError, ValidationError, FailedToCreateError } = require(
 
 /**
  * Authentication model class
+ * @class
  * @extends Model
+ * @property {number} id - Authentication ID (primary key)
+ * @property {number} user_id - User ID (foreign key)
+ * @property {string} password - Hashed password
+ * @property {string} password_salt - Password salt
+ * @property {string|null} password_verification_token - Verification token
+ * @property {Date|null} password_verification_token_expiry - Token expiry
+ * @property {Date|null} last_password_failure_date - Last failed login attempt
+ * @property {number} password_failures_since_last_success - Failure count
+ * @property {boolean} is_deleted - Soft delete flag
+ * @property {number|null} deleted_by_id - ID of user who deleted this record
+ * @property {Date|null} deleted_at - Deletion timestamp
+ * @property {string|null} password_reset_token - Reset token
+ * @property {Date|null} password_reset_expire_date - Reset token expiry
+ * @property {boolean|null} is_locked - Account lock status
+ * @property {Date|null} locked_date - Lock timestamp
+ *
  */
 class Authentication extends Model {
   /**
    * Create an Authentication instance
-   * @param {Partial<Authentication>} [props] - Authentication properties
+   * @param {Partial<Authentication_props>} [props] - Authentication properties
+   * @returns {Model<Authentication>||Object} Authentication instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
    */
   constructor( props ) {
     super( props );
@@ -78,7 +98,13 @@ class Authentication extends Model {
     this.default_order_by = 'ORDER BY auth.user_id ASC';
     this.instance = _props => new Authentication( _props );
     this.toJSON = () => {
-      const { password, password_salt, password_verification_token, password_reset_token, ...safeData } = super.toJSON();
+      const {
+        password,
+        password_salt,
+        password_verification_token,
+        password_reset_token,
+        ...safeData
+      } = super.toJSON();
       return safeData;
     };
   };
@@ -123,7 +149,7 @@ class Authentication extends Model {
    * Find authentication record by user ID
    * @param {number|string} user_id - User ID
    * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude from result
-   * @returns {Promise<Authentication|null>} Authentication instance or null
+   * @returns {Promise<Model<Authentication>|null>} Authentication instance or null
    */
   static async find_by_user_id( user_id, excludes = ['password', 'password_salt'] ) {
     return await new Authentication().find_one( { user_id }, excludes );
@@ -142,7 +168,7 @@ class Authentication extends Model {
   /**
    * Soft delete authentication record
    * @param {number|string} deleted_by_id - ID of user performing deletion
-   * @returns {Promise<Authentication>} Updated authentication instance
+   * @returns {Promise<Model<Authentication>>} Updated authentication instance
    * @throws {ValidationError} If deleted_by_id is invalid
    * @throws {NotFoundError} If record not found
    */
@@ -160,7 +186,7 @@ class Authentication extends Model {
 
   /**
    * Lock user account
-   * @returns {Promise<Authentication>} Updated authentication instance
+   * @returns {Promise<Model<Authentication>>} Updated authentication instance
    * @throws {NotFoundError} If record not found
    */
   async lock_account() {
@@ -172,7 +198,7 @@ class Authentication extends Model {
 
   /**
    * Unlock user account
-   * @returns {Promise<Authentication>} Updated authentication instance
+   * @returns {Promise<Model<Authentication>>} Updated authentication instance
    * @throws {NotFoundError} If record not found
    */
   async unlock_account() {
diff --git a/src/models/comment_reactions.model.js b/src/models/comment_reactions.model.js
new file mode 100755 (executable)
index 0000000..326b611
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * @file Comment reactions model for phase.comment_reactions table
+ * @module CommentReaction
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} CommentReaction
+ * @property {number} comment_id - Comment ID (foreign key to comments)
+ * @property {number} user_id - User ID (foreign key to users)
+ * @property {string} reaction - Reaction type (e.g., 'like', 'heart', 'laugh')
+ */
+
+/**
+ * Comment reactions model class
+ * @extends Model
+ */
+class CommentReaction extends Model {
+  /**
+   * Create a CommentReaction instance
+   * @param {Partial<CommentReaction>} [props] - Comment reaction properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.comment_reactions';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'cr';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = [ 'comment_id', 'user_id', 'reaction' ];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = [ 'comment_id', 'user_id' ];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `SELECT cr.comment_id, cr.user_id, cr.reaction
+                       FROM phase.comment_reactions cr`;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY cr.comment_id ASC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new CommentReaction( _props );
+  }
+
+  /**
+   * Add a comment reaction
+   * @param {number|string} comment_id - Comment ID
+   * @param {number|string} user_id - User ID
+   * @param {string} reaction - Reaction type
+   * @returns {Promise<CommentReaction>} Created or updated reaction instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async add( comment_id, user_id, reaction ) {
+    const comment_id_int = parseInt( comment_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( comment_id_int ) || isNaN( user_id_int ) || !reaction ) {
+      throw new ValidationError( 'comment_id, user_id, and reaction are required' );
+    }
+    const query_str = `
+        INSERT INTO phase.comment_reactions (comment_id, user_id, reaction)
+        VALUES ($1, $2, $3)
+        ON CONFLICT (comment_id, user_id) DO UPDATE SET reaction = EXCLUDED.reaction
+        RETURNING *;
+    `;
+    const values = [ comment_id_int, user_id_int, reaction ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    if ( !result ) throw new FailedToCreateError( 'Failed to add comment reaction' );
+    return new CommentReaction( result );
+  }
+
+  /**
+   * Remove a comment reaction
+   * @param {number|string} comment_id - Comment ID
+   * @param {number|string} user_id - User ID
+   * @returns {Promise<CommentReaction|null>} Deleted reaction instance or null
+   * @throws {ValidationError} If IDs are invalid
+   */
+  static async remove( comment_id, user_id ) {
+    const comment_id_int = parseInt( comment_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( comment_id_int ) || isNaN( user_id_int ) ) {
+      throw new ValidationError( 'comment_id and user_id must be valid integers' );
+    }
+    const query_str = `DELETE
+                       FROM phase.comment_reactions
+                       WHERE comment_id = $1
+                         AND user_id = $2
+                       RETURNING *;`;
+    const values = [ comment_id_int, user_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    return result ? new CommentReaction( result ) : null;
+  }
+
+  /**
+   * Find comment reactions by comment ID
+   * @param {number|string} comment_id - Comment ID
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<CommentReaction[]>} Array of reactions
+   */
+  static async find_by_comment_id( comment_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+    return await new CommentReaction().find_many( { comment_id }, excludes, order_by, limit, offset );
+  }
+}
+
+module.exports = CommentReaction;
\ No newline at end of file
diff --git a/src/models/comments.model.js b/src/models/comments.model.js
new file mode 100755 (executable)
index 0000000..a0c49ff
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * @file Comments model for phase.comments table
+ * @module Comment
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} Comment
+ * @property {number} id - Comment ID (primary key)
+ * @property {number} post_id - Post ID (foreign key to posts)
+ * @property {number} user_id - User ID (foreign key to users)
+ * @property {number|null} parent_id - Parent comment ID (foreign key to comments)
+ * @property {string} content - Comment content
+ * @property {Date} created_at - Creation timestamp
+ */
+
+/**
+ * Comments model class
+ * @extends Model
+ */
+class Comment extends Model {
+  /**
+   * Create a Comment instance
+   * @param {Partial<Comment>} [props] - Comment properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.comments';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'c';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = [ 'id', 'post_id', 'user_id', 'parent_id', 'content', 'created_at' ];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = [ 'id', 'post_id', 'user_id', 'created_at' ];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `
+        SELECT c.id,
+               c.post_id,
+               c.user_id,
+               c.parent_id,
+               c.content,
+               c.created_at,
+               concat(u.first_name, ' ', u.last_name) as user_name
+        FROM phase.comments c
+                 JOIN phase.users u ON c.user_id = u.id
+    `;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY c.created_at DESC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new Comment( _props );
+  }
+
+  /**
+   * Create a new comment
+   * @param {Omit<Comment, 'id'|'created_at'>} comment_data - Comment data
+   * @returns {Promise<Comment>} Created comment instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create( comment_data ) {
+    const { post_id, user_id, parent_id, content } = comment_data;
+    if ( !post_id || !user_id || !content ) {
+      throw new ValidationError( 'Missing required fields: post_id, user_id, content' );
+    }
+    const query_str = `
+        INSERT INTO phase.comments (post_id, user_id, parent_id, content)
+        VALUES ($1, $2, $3, $4)
+        RETURNING *;
+    `;
+    const values = [ post_id, user_id, parent_id || null, content ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    if ( !result ) throw new FailedToCreateError( 'Failed to create comment' );
+    return new Comment( result );
+  }
+
+  /**
+   * Find comments by post ID
+   * @param {number|string} post_id - Post ID
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<Comment[]>} Array of comments
+   */
+  static async find_by_post_id( post_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+    return await new Comment().find_many( { post_id }, excludes, order_by, limit, offset );
+  }
+}
+
+module.exports = Comment;
\ No newline at end of file
diff --git a/src/models/file.model.js b/src/models/file.model.js
new file mode 100755 (executable)
index 0000000..2c1a165
--- /dev/null
@@ -0,0 +1,135 @@
+/**
+ * @file Files model for phase.files table
+ * @module File
+ */
+
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} File
+ * @property {number} id - File 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., 'personal', 'family', 'specific')
+ * @property {number|null} created_by_id - ID of user who created this file
+ * @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 file
+ * @property {Date|null} deleted_at - Deletion timestamp
+ */
+
+/**
+ * Files model class
+ * @extends Model
+ */
+class File extends Model {
+  /**
+   * Create a File instance
+   * @param {Partial<File>} [props] - File properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.files';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'f';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = ['id', 'user_id', 'file_path', 'file_type', 'visibility', 'created_by_id', 'created_at', 'is_deleted', 'deleted_by_id', 'deleted_at'];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id'];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `
+        SELECT f.id,
+               f.user_id,
+               f.file_path,
+               f.file_type,
+               f.visibility,
+               f.created_by_id,
+               f.created_at,
+               f.is_deleted,
+               f.deleted_by_id,
+               f.deleted_at,
+               concat(u.first_name, ' ', u.last_name) as user_name
+        FROM phase.files f
+                 JOIN phase.users u ON f.user_id = u.id
+    `;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY f.created_at DESC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new File( _props );
+  }
+
+  /**
+   * Create a new file
+   * @param {Omit<File, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'> & {specific_users?: number[]}} file_data - File data
+   * @returns {Promise<File>} Created file instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create( file_data ) {
+    const {
+      user_id,
+      file_path,
+      file_type,
+      visibility = 'family',
+      created_by_id = null,
+      specific_users = []
+    } = file_data;
+    if (!user_id || !file_path || !file_type) {
+      throw new ValidationError( 'Missing required fields: user_id, file_path, file_type' );
+    }
+    return await this.prototype.with_transaction( async ( client ) => {
+      const query_str = `
+          INSERT INTO phase.files (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 || user_id];
+      const result = await phsdb.client_query( client, query_str, values, { plain:true } );
+      if (!result) throw new FailedToCreateError( 'Failed to create file' );
+      const file = new File( result );
+      if (visibility === 'specific' && specific_users?.length) {
+        const user_query = `INSERT INTO phase.file_users (file_id, user_id)
+                            VALUES ($1, $2);`;
+        for (const user_id of specific_users) {
+          await phsdb.client_query( client, user_query, [file.id, user_id] );
+        }
+      }
+      return file;
+    } );
+  }
+
+  /**
+   * Find file by file path
+   * @param {string} file_path - File path to search for
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @returns {Promise<File|null>} File instance or null
+   */
+  static async find_by_file_path( file_path, excludes = [] ) {
+    return await new File().find_one( { file_path }, excludes );
+  }
+
+  /**
+   * Soft delete file
+   * @param {number|string} deleted_by_id - ID of user performing deletion
+   * @returns {Promise<File>} Updated file 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 = File;
\ No newline at end of file
diff --git a/src/models/file_users.model.js b/src/models/file_users.model.js
new file mode 100755 (executable)
index 0000000..149fc04
--- /dev/null
@@ -0,0 +1,104 @@
+/**
+ * @file File users model for phase.file_users table
+ * @module FileUser
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} FileUser
+ * @property {number} file_id - File ID (foreign key to files)
+ * @property {number} user_id - User ID (foreign key to users)
+ */
+
+/**
+ * File users model class
+ * @extends Model
+ */
+class FileUser extends Model {
+  /**
+   * Create a FileUser instance
+   * @param {Partial<FileUser>} [props] - File user properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.file_users';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'fu';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = [ 'file_id', 'user_id' ];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = [ 'file_id', 'user_id' ];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `SELECT fu.file_id, fu.user_id
+                       FROM phase.file_users fu`;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY fu.file_id ASC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new FileUser( _props );
+  }
+
+  /**
+   * Add a file-user relation
+   * @param {number|string} file_id - File ID
+   * @param {number|string} user_id - User ID
+   * @returns {Promise<FileUser>} Created relation instance
+   * @throws {ValidationError} If IDs are invalid
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async add_relation( file_id, user_id ) {
+    const file_id_int = parseInt( file_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( file_id_int ) || isNaN( user_id_int ) ) {
+      throw new ValidationError( 'file_id and user_id must be valid integers' );
+    }
+    const query_str = `INSERT INTO phase.file_users (file_id, user_id)
+                       VALUES ($1, $2)
+                       RETURNING *;`;
+    const values = [ file_id_int, user_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    if ( !result ) throw new FailedToCreateError( 'Failed to add file-user relation' );
+    return new FileUser( result );
+  }
+
+  /**
+   * Remove a file-user relation
+   * @param {number|string} file_id - File ID
+   * @param {number|string} user_id - User ID
+   * @returns {Promise<FileUser|null>} Deleted relation instance or null
+   * @throws {ValidationError} If IDs are invalid
+   */
+  static async remove_relation( file_id, user_id ) {
+    const file_id_int = parseInt( file_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( file_id_int ) || isNaN( user_id_int ) ) {
+      throw new ValidationError( 'file_id and user_id must be valid integers' );
+    }
+    const query_str = `DELETE
+                       FROM phase.file_users
+                       WHERE file_id = $1
+                         AND user_id = $2
+                       RETURNING *;`;
+    const values = [ file_id_int, user_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    return result ? new FileUser( result ) : null;
+  }
+
+  /**
+   * Find file-user relations by file ID
+   * @param {number|string} file_id - File ID
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<FileUser[]>} Array of relations
+   */
+  static async find_by_file_id( file_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+    return await new FileUser().find_many( { file_id }, excludes, order_by, limit, offset );
+  }
+}
+
+module.exports = FileUser;
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
index 59e2c8e..635ff5b
@@ -1,7 +1,8 @@
-// noinspection ExceptionCaughtLocallyJS
+// noinspection JSUnusedGlobalSymbols
 
 /**
  * @file Unified database access interface
+ * @module Database
  */
 
 const model_cache = new Map();
@@ -12,639 +13,1649 @@ const { NotFoundError } = require( './model' );
  * @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
+ * @returns {Function} File model class
  */
-const get_media_model = () => get_model('media');
+const get_file_model = () => get_model( 'file' );
+
+/**
+ * @returns {Function} File users model class
+ */
+const get_file_users_model = () => get_model( 'file_users' );
 
 /**
  * @returns {Function} Post model class
  */
-const get_post_model = () => get_model('post');
+const get_post_model = () => get_model( 'post' );
+
+/**
+ * @returns {Function} Post files model class
+ */
+const get_post_files_model = () => get_model( 'post_files' );
+
+/**
+ * @returns {Function} Post users model class
+ */
+const get_post_users_model = () => get_model( 'post_users' );
+
+/**
+ * @returns {Function} Post reactions model class
+ */
+const get_post_reactions_model = () => get_model( 'post_reactions' );
+
+/**
+ * @returns {Function} Comments model class
+ */
+const get_comments_model = () => get_model( 'comments' );
+
+/**
+ * @returns {Function} Comment reactions model class
+ */
+const get_comment_reactions_model = () => get_model( 'comment_reactions' );
 
 /**
  * @returns {Function} Message group model class
  */
-const get_message_group_model = () => get_model('message_group');
+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');
+const get_message_group_members_model = () => get_model( 'message_group_members' );
 
 /**
  * @returns {Function} Message model class
  */
-const get_message_model = () => get_model('message');
+const get_message_model = () => get_model( 'message' );
+
+/**
+ * @returns {Function} Message files model class
+ */
+const get_message_files_model = () => get_model( 'message_files' );
+
+/**
+ * @returns {Function} Message reactions model class
+ */
+const get_message_reactions_model = () => get_model( 'message_reactions' );
 
 /**
  * Unified database interface
  * @type {Object}
  */
 const db = {
-  user: {
-    async create(user_data) {
+  user:{
+    /**
+     * Create a new user
+     * @param {Object} user_data - User data
+     * @returns {Promise<Object>} Created user instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async find_by_email(email, excludes) {
+    /**
+     * Find user by email
+     * @param {string} email - Email to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} User instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_by_nickname(nickname, excludes) {
+    /**
+     * Find user by nickname
+     * @param {string} nickname - Nickname to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} User instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_active(excludes, order_by, limit, offset) {
+    /**
+     * Find active users
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of user instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_deleted(excludes, order_by, limit, offset) {
+    /**
+     * Find deleted users
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of user instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find one user
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} User instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find many users
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of user instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async deactivate(id, deactivated_by_id) {
+    /**
+     * Deactivate a user
+     * @param {number} id - User ID
+     * @param {number} deactivated_by_id - ID of user performing deactivation
+     * @returns {Promise<Object>} Updated user instance
+     * @throws {NotFoundError} If user not found
+     * @throws {Error} If update fails
+     */
+    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) { // noinspection ExceptionCaughtLocallyJS
+          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;
       }
     },
-    async reactivate(id) {
+    /**
+     * Reactivate a user
+     * @param {number} id - User ID
+     * @returns {Promise<Object>} Updated user instance
+     * @throws {NotFoundError} If user not found
+     * @throws {Error} If update fails
+     */
+    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) { // noinspection ExceptionCaughtLocallyJS
+          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;
       }
     },
-    async soft_delete(id, deleted_by_id) {
+    /**
+     * Soft delete a user
+     * @param {number} id - User ID
+     * @param {number} deleted_by_id - ID of user performing deletion
+     * @returns {Promise<Object>} Updated user instance
+     * @throws {NotFoundError} If user not found
+     * @throws {Error} If update fails
+     */
+    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) { // noinspection ExceptionCaughtLocallyJS
+          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;
       }
     },
-    instance: () => new (get_user_model())()
+    instance:() => new (get_user_model())()
   },
-  phone_number: {
-    async create(phone_data) {
+  phone_number:{
+    /**
+     * Create a new phone number
+     * @param {Object} phone_data - Phone number data
+     * @returns {Promise<Object>} Created phone number instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async find_by_number(number, excludes) {
+    /**
+     * Find phone number by number
+     * @param {string} number - Phone number to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Phone number instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find one phone number
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Phone number instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find many phone numbers
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of phone number instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    instance: () => new (get_phone_number_model())()
+    instance:() => new (get_phone_number_model())()
   },
-  user_phone_numbers: {
-    async add_relation(user_id, phone_number_id) {
+  user_phone_numbers:{
+    /**
+     * Add a user-phone number relation
+     * @param {number} user_id - User ID
+     * @param {number} phone_number_id - Phone number ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async remove_relation(phone_number_id, user_id) {
+    /**
+     * Remove a user-phone number relation
+     * @param {number} phone_number_id - Phone number ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    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;
       }
     },
-    async find_by_ids(user_id, phone_number_id, excludes) {
+    /**
+     * Find user-phone number relation by IDs
+     * @param {number} user_id - User ID
+     * @param {number} phone_number_id - Phone number ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Relation instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
+    /**
+     * Find user-phone number relations by user ID
+     * @param {number} user_id - User ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    instance: () => new (get_user_phone_numbers_model())()
+    instance:() => new (get_user_phone_numbers_model())()
   },
-  address: {
-    async create(address_data) {
+  address:{
+    /**
+     * Create a new address
+     * @param {Object} address_data - Address data
+     * @returns {Promise<Object>} Created address instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async find_by_zip_code(zip_code, excludes) {
+    /**
+     * Find address by zip code
+     * @param {string} zip_code - Zip code to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Address instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find one address
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Address instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find many addresses
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of address instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    instance: () => new (get_address_model())()
+    instance:() => new (get_address_model())()
   },
-  user_addresses: {
-    async add_relation(user_id, address_id) {
+  user_addresses:{
+    /**
+     * Add a user-address relation
+     * @param {number} user_id - User ID
+     * @param {number} address_id - Address ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async remove_relation(address_id, user_id) {
+    /**
+     * Remove a user-address relation
+     * @param {number} address_id - Address ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    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;
       }
     },
-    async find_by_ids(user_id, address_id, excludes) {
+    /**
+     * Find user-address relation by IDs
+     * @param {number} user_id - User ID
+     * @param {number} address_id - Address ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Relation instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
+    /**
+     * Find user-address relations by user ID
+     * @param {number} user_id - User ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    instance: () => new (get_user_addresses_model())()
+    instance:() => new (get_user_addresses_model())()
   },
-  authentication: {
-    async create(auth_data) {
+  authentication:{
+    /**
+     * Create a new authentication record
+     * @param {Object} auth_data - Authentication data
+     * @returns {Promise<Object>} Created authentication instance
+     * @throws {Error} If creation fails
+     */
+    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;
       }
     },
-    async find_by_user_id(user_id, excludes) {
+    /**
+     * Find authentication record by user ID
+     * @param {number} user_id - User ID
+     * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude
+     * @returns {Promise<Authentication|null>} Authentication instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_by_reset_token(token, excludes) {
+    /**
+     * Find authentication record by reset token
+     * @param {string} token - Password reset token
+     * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude
+     * @returns {Promise<Object|null>} Authentication instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find one authentication record
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Authentication instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find many authentication records
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of authentication instances
+     * @throws {Error} If the query fails
+     */
+    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;
       }
     },
-    async lock_account(id) {
+    /**
+     * Lock an authentication account
+     * @param {number} id - Authentication ID
+     * @returns {Promise<Object>} Updated authentication instance
+     * @throws {NotFoundError} If record not found
+     * @throws {Error} If update fails
+     */
+    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) { // noinspection ExceptionCaughtLocallyJS
+          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;
       }
     },
-    async unlock_account(id) {
+    /**
+     * Unlock an authentication account
+     * @param {number} id - Authentication ID
+     * @returns {Promise<Object>} Updated authentication instance
+     * @throws {NotFoundError} If record not found
+     * @throws {Error} If update fails
+     */
+    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) { // noinspection ExceptionCaughtLocallyJS
+          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 an authentication record
+     * @param {number} id - Authentication ID
+     * @param {number} deleted_by_id - ID of user performing deletion
+     * @returns {Promise<Object>} Updated authentication instance
+     * @throws {NotFoundError} If record not found
+     * @throws {Error} If update fails
+     */
+    async soft_delete( id, deleted_by_id ) {
+      try {
+        const auth = await new (get_authentication_model())().find_one( { id }, [] );
+        if (!auth) { // noinspection ExceptionCaughtLocallyJS
+          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 }` );
+        throw error;
+      }
+    },
+    instance:() => new (get_authentication_model())()
+  },
+  role:{
+    /**
+     * Create a new role
+     * @param {Object} role_data - Role data
+     * @returns {Promise<Object>} Created role instance
+     * @throws {Error} If creation fails
+     */
+    async create( role_data ) {
+      try {
+        return await get_role_model().create( role_data );
+      } catch (error) {
+        logger.error( `Failed to create role: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find role by name
+     * @param {string} name - Role name
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Role instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_by_name( name, excludes ) {
+      try {
+        return await get_role_model().find_by_name( name, excludes );
+      } catch (error) {
+        logger.error( `Failed to find role by name: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find one role
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Role instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
+      try {
+        return await new (get_role_model())().find_one( where, excludes );
+      } catch (error) {
+        logger.error( `Failed to find one role: ${ error.message }` );
         throw error;
       }
     },
-    async soft_delete(id, deleted_by_id) {
+    /**
+     * Find many roles
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of role instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
       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);
+        return await new (get_role_model())().find_many( where, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to soft delete authentication: ${error.message}`);
+        logger.error( `Failed to find many roles: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_authentication_model())()
+    instance:() => new (get_role_model())()
   },
-  role: {
-    async create(role_data) {
+  user_roles:{
+    /**
+     * Add a user-role relation
+     * @param {number} user_id - User ID
+     * @param {number} role_id - Role ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    async add_relation( user_id, role_id ) {
       try {
-        return await get_role_model().create(role_data);
+        return await get_user_roles_model().add_relation( user_id, role_id );
       } catch (error) {
-        logger.error(`Failed to create role: ${error.message}`);
+        logger.error( `Failed to add user-role relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_name(name, excludes) {
+    /**
+     * Remove a user-role relation
+     * @param {number} role_id - Role ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove_relation( role_id, user_id ) {
       try {
-        return await get_role_model().find_by_name(name, excludes);
+        return await get_user_roles_model().remove_relation( role_id, user_id );
       } catch (error) {
-        logger.error(`Failed to find role by name: ${error.message}`);
+        logger.error( `Failed to remove user-role relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find user-role relation by IDs
+     * @param {number} user_id - User ID
+     * @param {number} role_id - Role ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Relation instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_by_ids( user_id, role_id, excludes ) {
       try {
-        return await new (get_role_model())().find_one(where, excludes);
+        return await get_user_roles_model().find_by_ids( user_id, role_id, excludes );
       } catch (error) {
-        logger.error(`Failed to find one role: ${error.message}`);
+        logger.error( `Failed to find user-role by IDs: ${ error.message }` );
         throw error;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find user-role relations by user ID
+     * @param {number} user_id - User ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    async find_by_user_id( user_id, excludes, order_by, limit, offset ) {
       try {
-        return await new (get_role_model())().find_many(where, excludes, order_by, limit, offset);
+        return await get_user_roles_model().find_by_user_id( user_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find many roles: ${error.message}`);
+        logger.error( `Failed to find user-roles by user ID: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_role_model())()
+    instance:() => new (get_user_roles_model())()
   },
-  user_roles: {
-    async add_relation(user_id, role_id) {
+  file:{
+    /**
+     * Create a new file
+     * @param {Object} file_data - File data
+     * @returns {Promise<Object>} Created file instance
+     * @throws {Error} If creation fails
+     */
+    async create( file_data ) {
+      try {
+        return await get_file_model().create( file_data );
+      } catch (error) {
+        logger.error( `Failed to create file: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find file by file path
+     * @param {string} file_path - File path to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} File instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_by_file_path( file_path, excludes ) {
+      try {
+        return await get_file_model().find_by_file_path( file_path, excludes );
+      } catch (error) {
+        logger.error( `Failed to find file by file path: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find one file
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} File instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
+      try {
+        return await new (get_file_model())().find_one( where, excludes );
+      } catch (error) {
+        logger.error( `Failed to find one file: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find many files
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of file instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
+      try {
+        return await new (get_file_model())().find_many( where, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find many files: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Soft delete a file
+     * @param {number} id - File ID
+     * @param {number} deleted_by_id - ID of user performing deletion
+     * @returns {Promise<Object>} Updated file instance
+     * @throws {NotFoundError} If the file is not found
+     * @throws {Error} If update fails
+     */
+    async soft_delete( id, deleted_by_id ) {
       try {
-        return await get_user_roles_model().add_relation(user_id, role_id);
+        const file = await new (get_file_model())().find_one( { id }, [] );
+        if (!file) { // noinspection ExceptionCaughtLocallyJS
+          throw new NotFoundError( 'File not found' );
+        }
+        return await file.soft_delete( deleted_by_id );
       } catch (error) {
-        logger.error(`Failed to add user-role relation: ${error.message}`);
+        logger.error( `Failed to soft delete file: ${ error.message }` );
         throw error;
       }
     },
-    async remove_relation(role_id, user_id) {
+    instance:() => new (get_file_model())()
+  },
+  file_users:{
+    /**
+     * Add a file-user relation
+     * @param {number} file_id - File ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    async add_relation( file_id, user_id ) {
       try {
-        return await get_user_roles_model().remove_relation(role_id, user_id);
+        return await get_file_users_model().add_relation( file_id, user_id );
       } catch (error) {
-        logger.error(`Failed to remove user-role relation: ${error.message}`);
+        logger.error( `Failed to add file-user relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_ids(user_id, role_id, excludes) {
+    /**
+     * Remove a file-user relation
+     * @param {number} file_id - File ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove_relation( file_id, user_id ) {
       try {
-        return await get_user_roles_model().find_by_ids(user_id, role_id, excludes);
+        return await get_file_users_model().remove_relation( file_id, user_id );
       } catch (error) {
-        logger.error(`Failed to find user-role by IDs: ${error.message}`);
+        logger.error( `Failed to remove file-user relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_user_id(user_id, excludes, order_by, limit, offset) {
+    /**
+     * Find file-user relations by file ID
+     * @param {number} file_id - File ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    async find_by_file_id( file_id, excludes, order_by, limit, offset ) {
       try {
-        return await get_user_roles_model().find_by_user_id(user_id, excludes, order_by, limit, offset);
+        return await get_file_users_model().find_by_file_id( file_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find user-roles by user ID: ${error.message}`);
+        logger.error( `Failed to find file-user relations by file ID: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_user_roles_model())()
+    instance:() => new (get_file_users_model())()
   },
-  media: {
-    async create(media_data) {
+  post:{
+    /**
+     * Create a new post
+     * @param {Object} post_data - Post data
+     * @returns {Promise<Object>} Created post instance
+     * @throws {Error} If creation fails
+     */
+    async create( post_data ) {
       try {
-        return await get_media_model().create(media_data);
+        return await get_post_model().create( post_data );
       } catch (error) {
-        logger.error(`Failed to create media: ${error.message}`);
+        logger.error( `Failed to create post: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_file_path(file_path, excludes) {
+    /**
+     * Find post by title
+     * @param {string} title - Title to search for
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Post instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_by_title( title, excludes ) {
       try {
-        return await get_media_model().find_by_file_path(file_path, excludes);
+        return await get_post_model().find_by_title( title, excludes );
       } catch (error) {
-        logger.error(`Failed to find media by file path: ${error.message}`);
+        logger.error( `Failed to find post by title: ${ error.message }` );
         throw error;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find one post
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Post instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
       try {
-        return await new (get_media_model())().find_one(where, excludes);
+        return await new (get_post_model())().find_one( where, excludes );
       } catch (error) {
-        logger.error(`Failed to find one media: ${error.message}`);
+        logger.error( `Failed to find one post: ${ error.message }` );
         throw error;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find many posts
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of post instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
       try {
-        return await new (get_media_model())().find_many(where, excludes, order_by, limit, offset);
+        return await new (get_post_model())().find_many( where, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find many media: ${error.message}`);
+        logger.error( `Failed to find many posts: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_media_model())()
+    /**
+     * Soft delete a post
+     * @param {number} id - Post ID
+     * @param {number} deleted_by_id - ID of user performing deletion
+     * @returns {Promise<Object>} Updated post instance
+     * @throws {NotFoundError} If post not found
+     * @throws {Error} If update fails
+     */
+    async soft_delete( id, deleted_by_id ) {
+      try {
+        const post = await new (get_post_model())().find_one( { id }, [] );
+        if (!post) { // noinspection ExceptionCaughtLocallyJS
+          throw new NotFoundError( 'Post not found' );
+        }
+        return await post.soft_delete( deleted_by_id );
+      } catch (error) {
+        logger.error( `Failed to soft delete post: ${ error.message }` );
+        throw error;
+      }
+    },
+    instance:() => new (get_post_model())()
   },
-  post: {
-    async create(post_data) {
+  post_files:{
+    /**
+     * Add a post-file relation
+     * @param {number} post_id - Post ID
+     * @param {number} file_id - File ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    async add_relation( post_id, file_id ) {
       try {
-        return await get_post_model().create(post_data);
+        return await get_post_files_model().add_relation( post_id, file_id );
       } catch (error) {
-        logger.error(`Failed to create post: ${error.message}`);
+        logger.error( `Failed to add post-file relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_title(title, excludes) {
+    /**
+     * Remove a post-file relation
+     * @param {number} post_id - Post ID
+     * @param {number} file_id - File ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove_relation( post_id, file_id ) {
       try {
-        return await get_post_model().find_by_title(title, excludes);
+        return await get_post_files_model().remove_relation( post_id, file_id );
       } catch (error) {
-        logger.error(`Failed to find post by title: ${error.message}`);
+        logger.error( `Failed to remove post-file relation: ${ error.message }` );
         throw error;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Find post-file relations by post ID
+     * @param {number} post_id - Post ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    async find_by_post_id( post_id, excludes, order_by, limit, offset ) {
       try {
-        return await new (get_post_model())().find_one(where, excludes);
+        return await get_post_files_model().find_by_post_id( post_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find one post: ${error.message}`);
+        logger.error( `Failed to find post-file relations by post ID: ${ error.message }` );
         throw error;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    instance:() => new (get_post_files_model())()
+  },
+  post_users:{
+    /**
+     * Add a post-user relation
+     * @param {number} post_id - Post ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    async add_relation( post_id, user_id ) {
       try {
-        return await new (get_post_model())().find_many(where, excludes, order_by, limit, offset);
+        return await get_post_users_model().add_relation( post_id, user_id );
       } catch (error) {
-        logger.error(`Failed to find many posts: ${error.message}`);
+        logger.error( `Failed to add post-user relation: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_post_model())()
+    /**
+     * Remove a post-user relation
+     * @param {number} post_id - Post ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove_relation( post_id, user_id ) {
+      try {
+        return await get_post_users_model().remove_relation( post_id, user_id );
+      } catch (error) {
+        logger.error( `Failed to remove post-user relation: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find post-user relations by post ID
+     * @param {number} post_id - Post ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    async find_by_post_id( post_id, excludes, order_by, limit, offset ) {
+      try {
+        return await get_post_users_model().find_by_post_id( post_id, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find post-user relations by post ID: ${ error.message }` );
+        throw error;
+      }
+    },
+    instance:() => new (get_post_users_model())()
   },
-  message_group: {
-    async create(message_group_data) {
+  post_reactions:{
+    /**
+     * Add a post reaction
+     * @param {number} post_id - Post ID
+     * @param {number} user_id - User ID
+     * @param {string} reaction - Reaction type
+     * @returns {Promise<Object>} Created or updated reaction instance
+     * @throws {Error} If creation fails
+     */
+    async add( post_id, user_id, reaction ) {
       try {
-        return await get_message_group_model().create(message_group_data);
+        return await get_post_reactions_model().add( post_id, user_id, reaction );
       } catch (error) {
-        logger.error(`Failed to create message group: ${error.message}`);
+        logger.error( `Failed to add post reaction: ${ error.message }` );
         throw error;
       }
     },
-    async find_one(where, excludes) {
+    /**
+     * Remove a post reaction
+     * @param {number} post_id - Post ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted reaction instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove( post_id, user_id ) {
       try {
-        return await new (get_message_group_model())().find_one(where, excludes);
+        return await get_post_reactions_model().remove( post_id, user_id );
       } catch (error) {
-        logger.error(`Failed to find one message group: ${error.message}`);
+        logger.error( `Failed to remove post reaction: ${ error.message }` );
         throw error;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find post reactions by post ID
+     * @param {number} post_id - Post ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of reactions
+     * @throws {Error} If the query fails
+     */
+    async find_by_post_id( post_id, excludes, order_by, limit, offset ) {
       try {
-        return await new (get_message_group_model())().find_many(where, excludes, order_by, limit, offset);
+        return await get_post_reactions_model().find_by_post_id( post_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find many message groups: ${error.message}`);
+        logger.error( `Failed to find post reactions by post ID: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_message_group_model())()
+    instance:() => new (get_post_reactions_model())()
   },
-  message_group_members: {
-    async add_relation(group_id, user_id) {
+  comments:{
+    /**
+     * Create a new comment
+     * @param {Object} comment_data - Comment data
+     * @returns {Promise<Object>} Created comment instance
+     * @throws {Error} If creation fails
+     */
+    async create( comment_data ) {
       try {
-        return await get_message_group_members_model().add_relation(group_id, user_id);
+        return await get_comments_model().create( comment_data );
       } catch (error) {
-        logger.error(`Failed to add message group member relation: ${error.message}`);
+        logger.error( `Failed to create comment: ${ error.message }` );
         throw error;
       }
     },
-    async remove_relation(user_id, group_id) {
+    /**
+     * Find comments by post ID
+     * @param {number} post_id - Post ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of comments
+     * @throws {Error} If the query fails
+     */
+    async find_by_post_id( post_id, excludes, order_by, limit, offset ) {
       try {
-        return await get_message_group_members_model().remove_relation(user_id, group_id);
+        return await get_comments_model().find_by_post_id( post_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to remove message group member relation: ${error.message}`);
+        logger.error( `Failed to find comments by post ID: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_ids(group_id, user_id, excludes) {
+    /**
+     * Find one comment
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Comment instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
       try {
-        return await get_message_group_members_model().find_by_ids(group_id, user_id, excludes);
+        return await new (get_comments_model())().find_one( where, excludes );
       } catch (error) {
-        logger.error(`Failed to find message group member by IDs: ${error.message}`);
+        logger.error( `Failed to find one comment: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_group_id(group_id, excludes, order_by, limit, offset) {
+    /**
+     * Find many comments
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of comment instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
       try {
-        return await get_message_group_members_model().find_by_group_id(group_id, excludes, order_by, limit, offset);
+        return await new (get_comments_model())().find_many( where, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find message group members by group ID: ${error.message}`);
+        logger.error( `Failed to find many comments: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_message_group_members_model())()
+    instance:() => new (get_comments_model())()
   },
-  message: {
-    async create(message_data) {
+  comment_reactions:{
+    /**
+     * Add a comment reaction
+     * @param {number} comment_id - Comment ID
+     * @param {number} user_id - User ID
+     * @param {string} reaction - Reaction type
+     * @returns {Promise<Object>} Created or updated reaction instance
+     * @throws {Error} If creation fails
+     */
+    async add( comment_id, user_id, reaction ) {
       try {
-        return await get_message_model().create(message_data);
+        return await get_comment_reactions_model().add( comment_id, user_id, reaction );
       } catch (error) {
-        logger.error(`Failed to create message: ${error.message}`);
+        logger.error( `Failed to add comment reaction: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_group_id(group_id, excludes, order_by, limit, offset) {
+    /**
+     * Remove a comment reaction
+     * @param {number} comment_id - Comment ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted reaction instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove( comment_id, user_id ) {
       try {
-        return await get_message_model().find_by_group_id(group_id, excludes, order_by, limit, offset);
+        return await get_comment_reactions_model().remove( comment_id, user_id );
       } catch (error) {
-        logger.error(`Failed to find messages by group ID: ${error.message}`);
+        logger.error( `Failed to remove comment reaction: ${ error.message }` );
         throw error;
       }
     },
-    async find_by_recipient_id(recipient_id, excludes, order_by, limit, offset) {
+    /**
+     * Find comment reactions by comment ID
+     * @param {number} comment_id - Comment ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of reactions
+     * @throws {Error} If the query fails
+     */
+    async find_by_comment_id( comment_id, excludes, order_by, limit, offset ) {
       try {
-        return await get_message_model().find_by_recipient_id(recipient_id, excludes, order_by, limit, offset);
+        return await get_comment_reactions_model().find_by_comment_id( comment_id, excludes, order_by, limit, offset );
       } catch (error) {
-        logger.error(`Failed to find messages by recipient ID: ${error.message}`);
+        logger.error( `Failed to find comment reactions by comment ID: ${ error.message }` );
         throw error;
       }
     },
-    async find_one(where, excludes) {
+    instance:() => new (get_comment_reactions_model())()
+  },
+  message_group:{
+    /**
+     * Create a new message group
+     * @param {Object} message_group_data - Message group data
+     * @returns {Promise<Object>} Created message group instance
+     * @throws {Error} If creation fails
+     */
+    async create( message_group_data ) {
       try {
-        return await new (get_message_model())().find_one(where, excludes);
+        return await get_message_group_model().create( message_group_data );
       } catch (error) {
-        logger.error(`Failed to find one message: ${error.message}`);
+        logger.error( `Failed to create message group: ${ error.message }` );
         throw error;
       }
     },
-    async find_many(where, excludes, order_by, limit, offset) {
+    /**
+     * Find one message group
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Message group instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
       try {
-        return await new (get_message_model())().find_many(where, excludes, order_by, limit, offset);
+        return await new (get_message_group_model())().find_one( where, excludes );
       } catch (error) {
-        logger.error(`Failed to find many messages: ${error.message}`);
+        logger.error( `Failed to find one message group: ${ error.message }` );
         throw error;
       }
     },
-    async mark_as_read(id) {
+    /**
+     * Find many message groups
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of message group instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
       try {
-        const message = await new (get_message_model())().find_one({ id }, []);
-        if (!message) throw new NotFoundError('Message not found');
+        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:{
+    /**
+     * Add a message group member relation
+     * @param {number} group_id - Message group ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    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;
+      }
+    },
+    /**
+     * Remove a message group member relation
+     * @param {number} user_id - User ID
+     * @param {number} [group_id] - Message group ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    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;
+      }
+    },
+    /**
+     * Find message group member relation by IDs
+     * @param {number} group_id - Message group ID
+     * @param {number} user_id - User ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Object|null>} Relation instance or null
+     * @throws {Error} If the query fails
+     */
+    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;
+      }
+    },
+    /**
+     * Find message group member relations by group ID
+     * @param {number} group_id - Message group ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    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:{
+    /**
+     * Create a new message
+     * @param {Object} message_data - Message data
+     * @returns {Promise<Object>} Created message instance
+     * @throws {Error} If creation fails
+     */
+    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;
+      }
+    },
+    /**
+     * Find messages by group ID
+     * @param {number} group_id - Group ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of messages
+     * @throws {Error} If the query fails
+     */
+    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;
+      }
+    },
+    /**
+     * Find messages by recipient ID
+     * @param {number} recipient_id - Recipient ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of messages
+     * @throws {Error} If the query fails
+     */
+    async find_by_recipient_id( recipient_id, excludes, order_by, limit, offset ) {
+      try {
+        return await get_message_model().find_by_recipient_id( recipient_id, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find messages by recipient ID: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find one message
+     * @param {Object} where - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @returns {Promise<Message|null>} Message instance or null
+     * @throws {Error} If the query fails
+     */
+    async find_one( where, excludes ) {
+      try {
+        return await new (get_message_model())().find_one( where, excludes );
+      } catch (error) {
+        logger.error( `Failed to find one message: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find many messages
+     * @param {Object} [where] - Conditions for the query
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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 message instances
+     * @throws {Error} If the query fails
+     */
+    async find_many( where, excludes, order_by, limit, offset ) {
+      try {
+        return await new (get_message_model())().find_many( where, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find many messages: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Mark a message as read
+     * @param {number} id - Message ID
+     * @returns {Promise<Object>} Updated message instance
+     * @throws {NotFoundError} If the message is not found
+     * @throws {Error} If update fails
+     */
+    async mark_as_read( id ) {
+      try {
+        const message = await new (get_message_model())().find_one( { id }, [] );
+        if (!message) { // noinspection ExceptionCaughtLocallyJS
+          throw new NotFoundError( 'Message not found' );
+        }
         return await message.mark_as_read();
       } catch (error) {
-        logger.error(`Failed to mark message as read: ${error.message}`);
+        logger.error( `Failed to mark message as read: ${ error.message }` );
+        throw error;
+      }
+    },
+    instance:() => new (get_message_model())()
+  },
+  message_files:{
+    /**
+     * Add a message-file relation
+     * @param {number} message_id - Message ID
+     * @param {number} file_id - File ID
+     * @returns {Promise<Object>} Created relation instance
+     * @throws {Error} If creation fails
+     */
+    async add_relation( message_id, file_id ) {
+      try {
+        return await get_message_files_model().add_relation( message_id, file_id );
+      } catch (error) {
+        logger.error( `Failed to add message-file relation: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Remove a message-file relation
+     * @param {number} message_id - Message ID
+     * @param {number} file_id - File ID
+     * @returns {Promise<Object|null>} Deleted relation instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove_relation( message_id, file_id ) {
+      try {
+        return await get_message_files_model().remove_relation( message_id, file_id );
+      } catch (error) {
+        logger.error( `Failed to remove message-file relation: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find message-file relations by message ID
+     * @param {number} message_id - Message ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of relations
+     * @throws {Error} If the query fails
+     */
+    async find_by_message_id( message_id, excludes, order_by, limit, offset ) {
+      try {
+        return await get_message_files_model().find_by_message_id( message_id, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find message-file relations by message ID: ${ error.message }` );
+        throw error;
+      }
+    },
+    instance:() => new (get_message_files_model())()
+  },
+  message_reactions:{
+    /**
+     * Add a message reaction
+     * @param {number} message_id - Message ID
+     * @param {number} user_id - User ID
+     * @param {string} reaction - Reaction type
+     * @returns {Promise<Object>} Created or updated reaction instance
+     * @throws {Error} If creation fails
+     */
+    async add( message_id, user_id, reaction ) {
+      try {
+        return await get_message_reactions_model().add( message_id, user_id, reaction );
+      } catch (error) {
+        logger.error( `Failed to add message reaction: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Remove a message reaction
+     * @param {number} message_id - Message ID
+     * @param {number} user_id - User ID
+     * @returns {Promise<Object|null>} Deleted reaction instance or null
+     * @throws {Error} If deletion fails
+     */
+    async remove( message_id, user_id ) {
+      try {
+        return await get_message_reactions_model().remove( message_id, user_id );
+      } catch (error) {
+        logger.error( `Failed to remove message reaction: ${ error.message }` );
+        throw error;
+      }
+    },
+    /**
+     * Find message reactions by message ID
+     * @param {number} message_id - Message ID
+     * @param {string[]} [excludes] - Fields to exclude
+     * @param {Object|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<Object[]>} Array of reactions
+     * @throws {Error} If the query fails
+     */
+    async find_by_message_id( message_id, excludes, order_by, limit, offset ) {
+      try {
+        return await get_message_reactions_model().find_by_message_id( message_id, excludes, order_by, limit, offset );
+      } catch (error) {
+        logger.error( `Failed to find message reactions by message ID: ${ error.message }` );
         throw error;
       }
     },
-    instance: () => new (get_message_model())()
+    instance:() => new (get_message_reactions_model())()
   }
 };
 
diff --git a/src/models/media.model.js b/src/models/media.model.js
deleted file mode 100644 (file)
index 586b110..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * @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
old mode 100644 (file)
new mode 100755 (executable)
index 32ff965..04ca92f
@@ -1,8 +1,9 @@
 /**
  * @file Message model for phase.messages table
+ * @module Message
  */
 
-const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model');
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' );
 
 /**
  * @typedef {Object} Message
@@ -14,6 +15,8 @@ const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('
  * @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
+ * @property {Object[]} attached_files - Array of attached file IDs
+ * @property {Object[]} reactions - Array of message reactions
  */
 
 /**
@@ -25,71 +28,105 @@ class Message extends Model {
    * Create a Message instance
    * @param {Partial<Message>} [props] - Message properties
    */
-  constructor(props) {
-    super(props);
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
     this.table = 'phase.messages';
+    /** @type {string} Table alias for queries */
     this.prepend = 'm';
-    this.default_columns = [
-      'id', 'sender_id', 'group_id', 'recipient_id', 'content',
-      'read', 'read_at', 'created_at'
-    ];
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = ['id', 'sender_id', 'group_id', 'recipient_id', 'content', 'read', 'read_at', 'created_at'];
+    /** @type {string[]} Columns excluded from updates */
     this.update_exclude_columns = ['id', 'created_at'];
+    /** @type {string} Base query for single record retrieval */
     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
+        SELECT m.id,
+               m.sender_id,
+               m.group_id,
+               m.recipient_id,
+               m.content,
+               m.read,
+               m.read_at,
+               m.created_at,
+               concat(se.first_name, ' ', se.last_name) as sender_name,
+               concat(re.first_name, ' ', re.last_name) as recipient_name,
+               concat(mg.name)                          as group_name,
+               (SELECT json_agg(json_build_object('file_id', mf.file_id))
+                FROM phase.message_files mf
+                WHERE mf.message_id = m.id)             as attached_files,
+               (SELECT json_agg(json_build_object('user_id', mr.user_id, 'reaction', mr.reaction))
+                FROM phase.message_reactions mr
+                WHERE mr.message_id = m.id)             as reactions
+        FROM phase.messages m
+                 INNER JOIN phase.users se ON se.id = m.sender_id
+                 LEFT OUTER JOIN phase.users re ON re.id = m.recipient_id
+                 LEFT OUTER JOIN phase.message_groups mg ON mg.id = m.group_id
     `;
+    /** @type {string} Base query for multiple record retrieval */
     this.base_list_query = this.base_query;
-    this.default_order_by = 'ORDER BY m.created_at DESC';
-    this.instance = _props => new Message(_props);
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY m.created_at ASC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new Message( _props );
   }
 
   /**
    * Create a new message
-   * @param {Omit<Message, 'id'|'created_at'|'read'|'read_at'>} message_data - Message data
+   * @param {Omit<Message, 'id'|'created_at'|'read'|'read_at'> & {file_ids?: number[]}} 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;
+  static async create( message_data ) {
+    const { sender_id, group_id, recipient_id, content, file_ids = [] } = 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');
+      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);
+    return await this.prototype.with_transaction( async ( client ) => {
+      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.client_query( client, query_str, values, { plain:true } );
+      if (!result) throw new FailedToCreateError( 'Failed to create message' );
+      const message = new Message( result );
+      if (file_ids?.length) {
+        const file_query = `INSERT INTO phase.message_files (message_id, file_id)
+                            VALUES ($1, $2);`;
+        for (const file_id of file_ids) {
+          await phsdb.client_query( client, file_query, [message.id, file_id] );
+        }
+      }
+      return message;
+    } );
   }
 
   /**
    * 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 {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - 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);
+  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 {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - 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);
+  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 );
   }
 
   /**
@@ -98,10 +135,7 @@ class Message extends Model {
    * @throws {NotFoundError} If record not found
    */
   async mark_as_read() {
-    return await this.update({
-      read: true,
-      read_at: new Date().toISOString()
-    });
+    return await this.update( { read:true, read_at:new Date().toISOString() } );
   }
 }
 
diff --git a/src/models/message_files.model.js b/src/models/message_files.model.js
new file mode 100755 (executable)
index 0000000..5ed9955
--- /dev/null
@@ -0,0 +1,104 @@
+/**
+ * @file Message files model for phase.message_files table
+ * @module MessageFile
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} MessageFile
+ * @property {number} message_id - Message ID (foreign key to messages)
+ * @property {number} file_id - File ID (foreign key to files)
+ */
+
+/**
+ * Message files model class
+ * @extends Model
+ */
+class MessageFile extends Model {
+  /**
+   * Create a MessageFile instance
+   * @param {Partial<MessageFile>} [props] - Message file properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.message_files';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'mf';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = [ 'message_id', 'file_id' ];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = [ 'message_id', 'file_id' ];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `SELECT mf.message_id, mf.file_id
+                       FROM phase.message_files mf`;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY mf.message_id ASC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new MessageFile( _props );
+  }
+
+  /**
+   * Add a message-file relation
+   * @param {number|string} message_id - Message ID
+   * @param {number|string} file_id - File ID
+   * @returns {Promise<MessageFile>} Created relation instance
+   * @throws {ValidationError} If IDs are invalid
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async add_relation( message_id, file_id ) {
+    const message_id_int = parseInt( message_id, 10 );
+    const file_id_int = parseInt( file_id, 10 );
+    if ( isNaN( message_id_int ) || isNaN( file_id_int ) ) {
+      throw new ValidationError( 'message_id and file_id must be valid integers' );
+    }
+    const query_str = `INSERT INTO phase.message_files (message_id, file_id)
+                       VALUES ($1, $2)
+                       RETURNING *;`;
+    const values = [ message_id_int, file_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    if ( !result ) throw new FailedToCreateError( 'Failed to add message-file relation' );
+    return new MessageFile( result );
+  }
+
+  /**
+   * Remove a message-file relation
+   * @param {number|string} message_id - Message ID
+   * @param {number|string} file_id - File ID
+   * @returns {Promise<MessageFile|null>} Deleted relation instance or null
+   * @throws {ValidationError} If IDs are invalid
+   */
+  static async remove_relation( message_id, file_id ) {
+    const message_id_int = parseInt( message_id, 10 );
+    const file_id_int = parseInt( file_id, 10 );
+    if ( isNaN( message_id_int ) || isNaN( file_id_int ) ) {
+      throw new ValidationError( 'message_id and file_id must be valid integers' );
+    }
+    const query_str = `DELETE
+                       FROM phase.message_files
+                       WHERE message_id = $1
+                         AND file_id = $2
+                       RETURNING *;`;
+    const values = [ message_id_int, file_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    return result ? new MessageFile( result ) : null;
+  }
+
+  /**
+   * Find message-file relations by message ID
+   * @param {number|string} message_id - Message ID
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<MessageFile[]>} Array of relations
+   */
+  static async find_by_message_id( message_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+    return await new MessageFile().find_many( { message_id }, excludes, order_by, limit, offset );
+  }
+}
+
+module.exports = MessageFile;
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/src/models/message_reactions.model.js b/src/models/message_reactions.model.js
new file mode 100755 (executable)
index 0000000..ee3aad3
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * @file Message reactions model for phase.message_reactions table
+ * @module MessageReaction
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} MessageReaction
+ * @property {number} message_id - Message ID (foreign key to messages)
+ * @property {number} user_id - User ID (foreign key to users)
+ * @property {string} reaction - Reaction type (e.g., 'like', 'heart', 'laugh')
+ */
+
+/**
+ * Message reactions model class
+ * @extends Model
+ */
+class MessageReaction extends Model {
+  /**
+   * Create a MessageReaction instance
+   * @param {Partial<MessageReaction>} [props] - Message reaction properties
+   */
+  constructor( props ) {
+    super( props );
+    /** @type {string} Database table name */
+    this.table = 'phase.message_reactions';
+    /** @type {string} Table alias for queries */
+    this.prepend = 'mr';
+    /** @type {string[]} Allowed columns for queries */
+    this.default_columns = [ 'message_id', 'user_id', 'reaction' ];
+    /** @type {string[]} Columns excluded from updates */
+    this.update_exclude_columns = [ 'message_id', 'user_id' ];
+    /** @type {string} Base query for single record retrieval */
+    this.base_query = `SELECT mr.message_id, mr.user_id, mr.reaction
+                       FROM phase.message_reactions mr`;
+    /** @type {string} Base query for multiple record retrieval */
+    this.base_list_query = this.base_query;
+    /** @type {string} Default ORDER BY clause */
+    this.default_order_by = 'ORDER BY mr.message_id ASC';
+    /** @type {Function} Function to instantiate a model */
+    this.instance = _props => new MessageReaction( _props );
+  }
+
+  /**
+   * Add a message reaction
+   * @param {number|string} message_id - Message ID
+   * @param {number|string} user_id - User ID
+   * @param {string} reaction - Reaction type
+   * @returns {Promise<MessageReaction>} Created or updated reaction instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async add( message_id, user_id, reaction ) {
+    const message_id_int = parseInt( message_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( message_id_int ) || isNaN( user_id_int ) || !reaction ) {
+      throw new ValidationError( 'message_id, user_id, and reaction are required' );
+    }
+    const query_str = `
+        INSERT INTO phase.message_reactions (message_id, user_id, reaction)
+        VALUES ($1, $2, $3)
+        ON CONFLICT (message_id, user_id) DO UPDATE SET reaction = EXCLUDED.reaction
+        RETURNING *;
+    `;
+    const values = [ message_id_int, user_id_int, reaction ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    if ( !result ) throw new FailedToCreateError( 'Failed to add message reaction' );
+    return new MessageReaction( result );
+  }
+
+  /**
+   * Remove a message reaction
+   * @param {number|string} message_id - Message ID
+   * @param {number|string} user_id - User ID
+   * @returns {Promise<MessageReaction|null>} Deleted reaction instance or null
+   * @throws {ValidationError} If IDs are invalid
+   */
+  static async remove( message_id, user_id ) {
+    const message_id_int = parseInt( message_id, 10 );
+    const user_id_int = parseInt( user_id, 10 );
+    if ( isNaN( message_id_int ) || isNaN( user_id_int ) ) {
+      throw new ValidationError( 'message_id and user_id must be valid integers' );
+    }
+    const query_str = `DELETE
+                       FROM phase.message_reactions
+                       WHERE message_id = $1
+                         AND user_id = $2
+                       RETURNING *;`;
+    const values = [ message_id_int, user_id_int ];
+    const result = await phsdb.query( query_str, values, { plain: true } );
+    return result ? new MessageReaction( result ) : null;
+  }
+
+  /**
+   * Find message reactions by message ID
+   * @param {number|string} message_id - Message ID
+   * @param {string[]} [excludes=[]] - Fields to exclude from result
+   * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration
+   * @param {number} [limit=100] - Maximum number of records
+   * @param {number} [offset=0] - Number of records to skip
+   * @returns {Promise<MessageReaction[]>} Array of reactions
+   */
+  static async find_by_message_id( message_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+    return await new MessageReaction().find_many( { message_id }, excludes, order_by, limit, offset );
+  }
+}
+
+module.exports = MessageReaction;
\ No newline at end of file
index 2c6d48d9b04e3a5734ab853744eb7284a5f93c57..776e09330dfab1ddfce7772eef8ab86ef19088c8 100755 (executable)
@@ -48,12 +48,28 @@ class ValidationError extends HttpError {
 
 /**
  * Base model class for database operations
+ * @class
+ * @method where_clause - Build WHERE clause
+ * @method build_where - Filter WHERE object to valid columns
+ * @method get_base_list_query - Get base list query
+ * @method create_update_fields - Create update fields string and values
+ * @method instance - Function to instantiate a model
+ * @method find_one - Find one record
+ * @method find_many - Find many records
+ * @method update - Update the record
+ * @method toJSON - Serialize to JSON
+ * @method with_transaction - Execute transaction
+ * @property {string} table - Database table name
+ * @property {string[]} default_columns - Allowed columns for queries
+ * @property {string[]} update_exclude_columns - Columns excluded from updates
+ * @property {string} prepend - Table alias for queries
+ * @property {string} base_query - Base query for single record retrieval
+ * @property {string} base_list_query - Base query for multiple record retrieval
+ * @property {string|undefined} default_order_by - Default ORDER BY clause
+ * @property {Function} instance - Function to instantiate a model
+ * @property {string|undefined} group_by - GROUP BY clause
  */
 class Model {
-  /**
-   * Create a model instance
-   * @param {Object} [props] - Properties to initialize the model
-   */
   constructor( props ) {
     props && Object.keys( props ).forEach( c => {
       this[c] = props[c];
@@ -72,7 +88,7 @@ class Model {
     this.base_list_query = '';
     /** @type {string|undefined} Default ORDER BY clause */
     this.default_order_by = undefined;
-    /** @type {Function} Function to instantiate a model */
+    /** @type {Function} Function to instantiate a model*/
     this.instance = _props => new Model( _props );
     /** @type {string|undefined} GROUP BY clause */
     this.group_by = undefined;
@@ -113,7 +129,7 @@ class Model {
    * Find one record
    * @param {Object} where - Conditions for the WHERE clause
    * @param {string[]} [excludes] - Fields to exclude from result
-   * @returns {Promise<Object|null>} Found record or null
+   * @returns {Promise<Model|null>} Found record or null
    */
   async find_one( where, excludes = [] ) {
     const { keys, values } = this.build_where( where, this.default_columns );
@@ -128,24 +144,6 @@ class Model {
     return found;
   };
 
-  /**
-   * Find one record (raw data)
-   * @param {Object} where - Conditions for the WHERE clause
-   * @param {string[]} [excludes] - Fields to exclude from result
-   * @returns {Promise<Object|null>} Found record or null
-   */
-  async find_one_simple( where, excludes = [] ) {
-    const { keys, values } = this.build_where( where, this.default_columns );
-    const result = await phsdb.query(
-      `${ this.base_query } ${ this.where_clause( keys, this.prepend ) } ${ this.group_by ? this.group_by : '' }`,
-      values,
-      { plain:true }
-    );
-    if (!result) return null;
-    excludes?.forEach( e => delete result[e] );
-    return result;
-  };
-
   /**
    * Get base list query
    * @returns {string} Base list query
@@ -185,43 +183,11 @@ class Model {
     return res;
   };
 
-  /**
-   * Find many records (raw data)
-   * @param {Object} [where] - Conditions for the WHERE clause
-   * @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<Object[]>} Array of records
-   */
-  async find_many_lite( where, excludes = [], order_by = null, limit = 100, offset = 0 ) {
-    const { keys, values } = this.build_where( where, this.default_columns );
-    values.push( limit, offset );
-    const results = await phsdb.query(
-      `
-        ${ this.get_base_list_query() }
-        ${ this.where_clause( keys, this.prepend ) }
-        ${ this.group_by ? this.group_by : '' }
-        ${ order_by ? `ORDER BY ${ order_by.name } ${ order_by.direction ?? 'asc' }` : this.default_order_by ?? '' }
-        LIMIT $${ values.length - 1 } OFFSET $${ values.length }
-      `,
-      values
-    );
-    const res = [];
-    for (const result of results) {
-      const data_values = {};
-      Object.keys( result ).forEach( k => data_values[k] = result[k] );
-      excludes?.forEach( e => delete data_values[e] );
-      res.push( data_values );
-    }
-    return res;
-  };
-
   /**
    * Update the record
    * @param {Object} params - Fields to update
    * @param {string} [identifier='id'] - Identifier field for update
-   * @returns {Promise<Object>} Updated record
+   * @returns {Promise<Model|Object>} Updated record
    * @throws {ValidationError} If no valid fields are provided or ID is missing
    * @throws {NotFoundError} If record is not found
    */
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 116af0f..1de91c5
@@ -1,13 +1,15 @@
+// noinspection JSUnusedGlobalSymbols
+
 /**
  * @file User model for phase.users table
  */
 
-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');
+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
@@ -33,8 +35,8 @@ const createHttpError = require('http-errors');
  * @extends Model
  */
 class User extends Model {
-  constructor(props) {
-    super(props);
+  constructor( props ) {
+    super( props );
     this.table = 'phase.users';
     this.prepend = 'u';
     this.default_columns = [
@@ -44,36 +46,71 @@ 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, a.password, a.password_salt,
-               a.is_locked, a.locked_date,
-               (select json_agg(r) from phase.user_roles ur inner join phase.roles r on ur.role_id = r.id ) as roles
+        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,
+               a.password,
+               a.password_salt,
+               a.is_locked,
+               a.locked_date,
+               (select json_agg(r)
+                from phase.roles r
+                         inner join phase.user_roles ur on r.id = ur.role_id
+                where ur.user_id = u.id) as roles
         FROM phase.users u
-            inner join phase.authentication a on u.id = a.user_id
+                 inner join phase.authentication a on u.id = a.user_id
     `;
     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, a.password, a.password_salt,
-               a.is_locked, a.locked_date
+        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,
+               a.password,
+               a.password_salt,
+               a.is_locked,
+               a.locked_date,
+               (select json_agg(r)
+                from phase.roles r
+                         inner join phase.user_roles ur on r.id = ur.role_id
+                where ur.user_id = u.id) as roles
         FROM phase.users u
                  inner join phase.authentication a on u.id = a.user_id
     `;
     this.default_order_by = 'ORDER BY u.email ASC';
-    this.instance = _props => new User(_props);
+    this.instance = _props => new User( _props );
     this.toJSON = () => {
       const { password, password_salt, ...safe_data } = super.toJSON();
       return safe_data;
     };
   };
 
-  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,
@@ -85,141 +122,142 @@ 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);
-  }
-
-  async get_user_roles() {
-    const query_str = `
-        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
-    `;
-    const result = await phsdb.query(query_str, [this.id]);
-    return result.map(r => r.name);
+    const result = await phsdb.query( query_str, values, { plain:true } );
+    if (!result) throw new ValidationError( 'Failed to create user' );
+    return new User( result );
   }
 
-  static async find_by_email(email, excludes = []) {
-    return await new User().find_one({ email }, excludes);
+  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_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_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');
+  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;
-    });
+    } );
+  }
+
+  async get_user_roles() {
+    const query_str = `
+        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
+    `;
+    const result = await phsdb.query( query_str, [this.id] );
+    return result.map( r => r.name );
   }
 
-  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
+    } );
   }
 
   async reactivate() {
-    return await this.update({
-      is_active: true,
-      deactivated_at: null,
-      deactivated_by_id: null
-    });
+    return await this.update( {
+      is_active:true,
+      deactivated_at:null,
+      deactivated_by_id:null
+    } );
   }
 
-  async comparePassword(password) {
-    return password ? bcrypt.compareSync(password, this.password) : false;
+  async comparePassword( password ) {
+    return password ? bcrypt.compareSync( password, this.password ) : false;
   }
 
   async createToken() {
-    const auth = await db.authentication.find_by_user_id(this.id);
+    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
-    });
+    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()
-    });
+    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()
-    });
+    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
-    });
+  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');
+  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
+    } );
   }
 
   is_active() {
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index a4f8fb9635d65de23d00fda77e5109ac64a1d0f9..1f689bf9dfb2abf1844910ff1bedc18411cebdc4 100755 (executable)
@@ -23,7 +23,7 @@ const json_slimify = (key, value) => {
 // noinspection JSUnusedLocalSymbols
 const log_query = (text, values, options={}) => {
   try{
-    // logger.debug(`log_query: ${text} -- With values: ${JSON.stringify(values,json_slimify)}`); // Incase you need to see the values and properties.
+    // logger.debug(`log_query: ${text} -- With values: ${JSON.stringify(values,json_slimify)}`); // In case you need to see the values and properties.
     const formattedQuery = pgp.as.format(text, values);
     logger.debug(
       `[Executing Query] ${formattedQuery} -- With values: ${JSON.stringify(
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 54078d4..2c19106
@@ -1,8 +1,9 @@
-const express = require('express');
+const express = require( 'express' );
 const router = express.Router();
-const authController = require('../controllers/auth.controller');
+const authController = require( '../controllers/auth.controller' );
 
 module.exports = () => {
-    router.post('/authenticate', authController.authenticate);
-    return router;
+  router.post( '/authenticate', authController.authenticate );
+  router.get( '/logout', authController.logout );
+  return router;
 };
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/src/routes/file_users.routes.js b/src/routes/file_users.routes.js
new file mode 100755 (executable)
index 0000000..745efa2
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * @file File users routes configuration
+ * @module FileUsersRoutes
+ */
+
+const express = require( 'express' );
+const router = express.Router();
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const file_users_controller = require( '../controllers/file_users.controller' );
+
+/**
+ * Configure file users routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with file users routes
+ */
+module.exports = ( passport ) => {
+  router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), file_users_controller.add_relation );
+  router.delete( '/:file_id/:user_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), file_users_controller.remove_relation );
+  router.get( '/file/:file_id', validate_auth( passport ), file_users_controller.find_by_file_id );
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/files.routes.js b/src/routes/files.routes.js
new file mode 100755 (executable)
index 0000000..57cfc90
--- /dev/null
@@ -0,0 +1,15 @@
+const express = require( 'express' );
+const router = express.Router();
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const files_controller = require( '../controllers/files.controller' );
+
+module.exports = ( passport ) => {
+  router.post( '/create', files_controller.uploadFile, validate_auth( passport ), files_controller.create );
+  router.get( '/file_path/:file_path', validate_auth( passport ), files_controller.find_by_file_path );
+  router.get( '/:id', validate_auth( passport ), files_controller.find_one );
+  router.get( '/', validate_auth( passport ), files_controller.find_many );
+  router.put( '/:id', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), files_controller.update );
+  router.put( '/:id/soft_delete', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), files_controller.soft_delete );
+  router.get( '/:id/view', validate_auth( passport ), files_controller.get_file );
+  return router;
+};
\ No newline at end of file
index 0734afe23d2738e969e2d0b904324e5fd77fe870..9df708677ed235ca4b1035009f707d9c784b4832 100755 (executable)
@@ -1,8 +1,17 @@
+/**
+ * @file Main routes configuration
+ * @module Routes
+ */
+
 const express = require( 'express' );
 const router = express.Router();
 
+/**
+ * Configure API routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with API routes
+ */
 module.exports.APIRoutes = ( passport ) => {
-
   router.use( '/git', require( './git.routes' )( passport ) );
   router.use( '/docker', require( './docker.routes' )( passport ) );
   router.use( '/vpn', require( './vpn.routes' )( passport ) );
@@ -14,11 +23,15 @@ module.exports.APIRoutes = ( passport ) => {
   router.use( '/authentication', require( './authentication.routes' )( passport ) );
   router.use( '/role', require( './role.routes' )( passport ) );
   router.use( '/user_roles', require( './user_roles.routes' )( passport ) );
-  router.use('/media', require('./media.routes')(passport));
-  router.use('/post', require('./post.routes')(passport));
-  router.use('/message_group', require('./message_group.routes')(passport));
-  router.use('/message_group_members', require('./message_group_members.routes')(passport));
-  router.use('/message', require('./message.routes')(passport));
-  router.use('/auth', require('./auth.routes')());
+  router.use( '/file', require( './files.routes' )( passport ) );
+  router.use( '/file_users', require( './file_users.routes' )( passport ) );
+  router.use( '/post', require( './post.routes' )( passport ) );
+  router.use( '/post_files', require( './post_files.routes' )( passport ) );
+  router.use( '/post_users', require( './post_users.routes' )( passport ) );
+  router.use( '/message_group', require( './message_group.routes' )( passport ) );
+  router.use( '/message_group_members', require( './message_group_members.routes' )( passport ) );
+  router.use( '/message', require( './message.routes' )( passport ) );
+  router.use( '/message_files', require( './message_files.routes' )( passport ) );
+  router.use( '/auth', require( './auth.routes' )() );
   return router;
-};
+};
\ No newline at end of file
diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js
deleted file mode 100644 (file)
index 01a1e8d..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @file Media routes configuration
- */
-
-const express = require('express');
-const router = express.Router();
-const { validate_auth } = 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', validate_auth(passport), media_controller.create);
-  router.get('/file_path/:file_path', validate_auth(passport), media_controller.find_by_file_path);
-  router.get('/:id', validate_auth(passport), media_controller.find_one);
-  router.get('/', validate_auth(passport), media_controller.find_many);
-  router.put('/:id', validate_auth(passport), media_controller.update);
-  router.put('/:id/soft_delete', validate_auth(passport), media_controller.soft_delete);
-  return router;
-};
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
index 52d4fbb..0c874bd
@@ -2,22 +2,23 @@
  * @file Message routes configuration
  */
 
-const express = require('express');
+const express = require( 'express' );
 const router = express.Router();
-const { validate_auth } = require('../middleware/routeHelpers');
-const message_controller = require('../controllers/message.controller');
+const { validate_auth, restrictToRoles } = 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', validate_auth(passport), message_controller.create);
-  router.get('/group/:group_id', validate_auth(passport), message_controller.find_by_group_id);
-  router.get('/recipient/:recipient_id', validate_auth(passport), message_controller.find_by_recipient_id);
-  router.get('/:id', validate_auth(passport), message_controller.find_one);
-  router.get('/', validate_auth(passport), message_controller.find_many);
-  router.put('/:id/mark_as_read', validate_auth(passport), message_controller.mark_as_read);
+module.exports = ( passport ) => {
+  router.post( '/create', validate_auth( passport ), message_controller.create );
+  router.get( '/group/:group_id', validate_auth( passport ), message_controller.find_by_group_id );
+  router.get( '/recipient/:recipient_id', validate_auth( passport ), message_controller.find_by_recipient_id );
+  router.get( '/:id', validate_auth( passport ), message_controller.find_one );
+  router.get( '/', validate_auth( passport ), message_controller.find_many );
+  router.put( '/:id/mark_as_read', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), message_controller.mark_as_read );
+  router.post( '/:messageId/reactions', validate_auth( passport ), message_controller.add_reaction );
   return router;
 };
\ No newline at end of file
diff --git a/src/routes/message_files.routes.js b/src/routes/message_files.routes.js
new file mode 100755 (executable)
index 0000000..952e9cd
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * @file Message files routes configuration
+ * @module MessageFilesRoutes
+ */
+
+const express = require( 'express' );
+const router = express.Router();
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const message_files_controller = require( '../controllers/message_files.controller' );
+
+/**
+ * Configure message files routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with message files routes
+ */
+module.exports = ( passport ) => {
+  router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), message_files_controller.add_relation );
+  router.delete( '/:message_id/:file_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), message_files_controller.remove_relation );
+  router.get( '/message/:message_id', validate_auth( passport ), message_files_controller.find_by_message_id );
+  return router;
+};
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/src/routes/users.js b/src/routes/users.js
deleted file mode 100755 (executable)
index 623e430..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-var express = require('express');
-var router = express.Router();
-
-/* GET users listing. */
-router.get('/', function(req, res, next) {
-  res.send('respond with a resource');
-});
-
-module.exports = router;
old mode 100644 (file)
new mode 100755 (executable)