]> PHS Git Server - phs-api.git/commitdiff
Adding necessary api files.
authorcharleswrayjr <charleswrayjr@gmail.com>
Thu, 4 Sep 2025 06:46:13 +0000 (01:46 -0500)
committercharleswrayjr <charleswrayjr@gmail.com>
Thu, 4 Sep 2025 06:46:13 +0000 (01:46 -0500)
23 files changed:
.idea/runConfigurations/bin_www.xml [new file with mode: 0644]
api.config.cjs [new file with mode: 0755]
app.js
package.json
routes/index.js [deleted file]
routes/users.js [deleted file]
run-api.sh [new file with mode: 0755]
src/config/dbConfig.json [new file with mode: 0644]
src/config/default.json [new file with mode: 0644]
src/config/secret_key_gen.js [new file with mode: 0644]
src/controllers/git.controller.js [new file with mode: 0644]
src/db.js [new file with mode: 0644]
src/middleware/custom-startup.js [new file with mode: 0644]
src/middleware/loggerMiddleWare.js [new file with mode: 0644]
src/middleware/multer.js [new file with mode: 0644]
src/middleware/passport.js [new file with mode: 0644]
src/middleware/routeHelpers.js [new file with mode: 0644]
src/models/model.js [new file with mode: 0644]
src/phsdb.js [new file with mode: 0644]
src/routes/git.routes.js [new file with mode: 0644]
src/routes/index.js [new file with mode: 0644]
src/routes/users.js [new file with mode: 0644]
src/swagger.json [new file with mode: 0644]

diff --git a/.idea/runConfigurations/bin_www.xml b/.idea/runConfigurations/bin_www.xml
new file mode 100644 (file)
index 0000000..1afb596
--- /dev/null
@@ -0,0 +1,11 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="bin/www" type="NodeJSConfigurationType" path-to-js-file="bin/www" working-dir="$PROJECT_DIR$">
+    <envs>
+      <env name="DEBUG" value="phs-api:*" />
+    </envs>
+    <EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
+      <browser url="http://localhost:3000/" />
+    </EXTENSION>
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/api.config.cjs b/api.config.cjs
new file mode 100755 (executable)
index 0000000..caaf5c0
--- /dev/null
@@ -0,0 +1,25 @@
+const path = require("path");
+
+module.exports = {
+  apps: [
+    {
+      name: "phs-api",
+      script: "/usr/src/app/app.js", // Your entry point
+      instances: 1,
+      autorestart: true, // THIS is the important part, this will tell PM2 to restart your app if it falls over
+      max_memory_restart: "3G",
+      env: {
+        PHS_ENV: "DEV",
+        NODE_ENV: "DEVELOPMENT",
+      },
+      env_TEST: {
+        PHS_ENV: "TEST",
+        NODE_ENV: "PRODUCTION",
+      },
+      env_PRODUCTION: {
+        PHS_ENV: "PRODUCTION",
+        NODE_ENV: "PRODUCTION",
+      },
+    },
+  ],
+};
diff --git a/app.js b/app.js
index d187f73a03641759af0be1d7b6e8b55e6c080f2e..5c7212e470008e885f46ba878a8f30f9b63cf65e 100644 (file)
--- a/app.js
+++ b/app.js
-var express = require('express');
-var path = require('path');
-var cookieParser = require('cookie-parser');
-var logger = require('morgan');
+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 log4js = require( './src/middleware/loggerMiddleWare' );
+const passport = require( 'passport' );
+let hookJWTStrategy = require( './src/middleware/passport' );
+const cors = require( 'cors' );
+const port = normalizePort( process.env.PORT || '3601' );
+const runOnce = require ( './src/middleware/custom-startup' );
+const Routes = require( './src/routes' );
+const phsdb = require( './src/phsdb' );
+const { Client } = require('ssh2');
+const fs = require('fs');
 
-var indexRouter = require('./routes/index');
-var usersRouter = require('./routes/users');
+const buffer = require('buffer');
+global.Blob = buffer.Blob
 
-var app = express();
+const app = express();
+const http = require( 'http' ).createServer( app );
 
-app.use(logger('dev'));
-app.use(express.json());
-app.use(express.urlencoded({ extended: false }));
+app.set('etag', false)
+
+app.use((req, res, next) => {
+  res.set('Cache-Control', 'no-store');
+  next()
+});
+
+global.phsdb = phsdb;
+
+// noinspection JSCheckFunctionSignatures
+app.use( bodyParser.json( {
+  limit:'50mb', extended:true
+} ) );
+app.use( bodyParser.urlencoded( {
+  extended:true,
+  limit: '50mb'
+} ) );
 app.use(cookieParser());
-app.use(express.static(path.join(__dirname, 'public')));
 
-app.use('/', indexRouter);
-app.use('/users', usersRouter);
+//Setting up log4js
+const logger = log4js.default;
+
+// Set log level based on the active environment for global logger
+logger.level = 'warn';
+
+switch (process.env.PHS_ENV) {
+  case 'DEV':
+    logger.level = 'debug';
+    break;
+  case 'UNIT_TEST':
+    logger.level = 'info';
+    break;
+  default:
+    break;
+}
+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
+
+//Initialize passport
+app.use( passport.initialize() );
+hookJWTStrategy( passport );
+
+//Run custom startup code once
+runOnce().catch( err => logger.error( err ) );
+
+//Set express body parsing options.
+app.use( compression() );
+app.use( express.json() );
+app.use( express.urlencoded( { extended:false } ) );
+app.use(cookieParser());
+
+// 2FA Cors:
+app.use(cors({
+  exposedHeaders: ['LastFetchDateTime', 'Authentication'],
+  origin: [
+    // Home
+    'http://localhost:30005', // 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.roto-versal.com',
+    'https://customer-t.roto-versal.com',
+    'https://customer-demo.roto-versal.com',
+    'https://customer.roto-versal.com',
+    // Admin (local dev)
+    'http://localhost:8100',
+    'http://localhost:30007',
+    'https://admin-t.roto-versal.com',
+    'https://admin-f.roto-versal.com',
+    'https://admin-demo.roto-versal.com',
+    'https://admin.roto-versal.com',
+    // App (local dev)
+    'http://localhost:8101',
+    'https://app-t.roto-versal.com',
+    'https://app-f.roto-versal.com',
+    'https://app-demo.roto-versal.com',
+    'https://app.roto-versal.com',
+    // Public website
+    'https://www-t.roto-versal.com',
+    'https://www.roto-versal.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,
+}));
+
+//Send the current server date/time for each request
+app.use( function ( req, res, next ) {
+  res.setHeader( 'LastFetchDateTime', new Date().toLocaleString() );
+  next();
+} );
+
+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 );
+
+
+process.on('uncaughtException', function(err) {
+  logger.error( '*************uncaughtException******' );
+  logger.error( err );
+});
+
+process.on('unhandledRejection', (err) => {
+  logger.error( '*************unhandledRejection******' );
+  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();
+  else {
+    logger.debug(req.originalUrl)
+    logger.debug(req.headers['x-forwarded-for'])
+    next( createError( 404 ) );
+  }
+
+} );
+
+function normalizePort( val ) {
+  const port = parseInt( val, 10 );
+
+  if (isNaN( port )) {
+    // named pipe
+    return val;
+  }
+
+  if (port >= 0) {
+    // port number
+    return port;
+  }
+
+  return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError( error ) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
+
+  // handle specific listen errors with friendly messages
+  switch (error.code) {
+    case 'EACCES':
+      logger.error( bind + ' requires elevated privileges' );
+      process.exit( 1 );
+      break;
+    case 'EADDRINUSE':
+      logger.error( bind + ' is already in use' );
+      process.exit( 1 );
+      break;
+    default:
+      throw error;
+  }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+  const addr = http.address();
+  const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
+  logger.info( 'Listening on ' + bind );
+}
+
+// error handler
+const errorHandler = ( err, req, res, next ) => {
+  logger.error( '*************errorHandler******' );
+  if (err) {
+    logger.error( err );
+    // set locals, only providing error in DEVELOPMENT
+    res.locals.message = err.message;
+    res.locals.error = req.app.get( 'env' ) === 'DEVELOPMENT' ? err : {};
+    // render the error page
+    res.status( err.status || 500 );
+    res.send(err);
+  } else next()
+};
+
+app.use( errorHandler );
+
+console.log('running on ' + port);
 
 module.exports = app;
index becff01128abaa31055f6e8a21ba8603da25cce7..ed77ba239688e52117bb39d9f459169fe8e17428 100644 (file)
@@ -1,14 +1,31 @@
 {
   "name": "phs-api",
-  "version": "0.0.0",
+  "version": "1.0.0",
   "private": true,
   "scripts": {
-    "start": "node ./bin/www"
+    "start-debug": "NODE_ENV=\"DEVELOPMENT\" nodemon --trace-warnings --inspect=0.0.0.0:9229 app.js"
   },
   "dependencies": {
+    "body-parser": "^2.2.0",
     "cookie-parser": "~1.4.4",
+    "cors": "^2.8.5",
     "debug": "~2.6.9",
-    "express": "~4.16.1",
-    "morgan": "~1.9.1"
+    "express": "5.0.0",
+    "express-compression": "^1.0.2",
+    "http-errors": "~1.6.3",
+    "jsonwebtoken": "^9.0.2",
+    "log4js": "^6.9.1",
+    "morgan": "~1.9.1",
+    "multer": "^2.0.2",
+    "passport": "^0.7.0",
+    "passport-jwt": "^4.0.1",
+    "pg": "^8.16.3",
+    "pg-promise": "^11.15.0",
+    "pug": "3.0.3",
+    "ssh2": "^1.17.0",
+    "swagger-ui-express": "^5.0.1"
+  },
+  "devDependencies": {
+    "jest": "^30.0.5"
   }
 }
diff --git a/routes/index.js b/routes/index.js
deleted file mode 100644 (file)
index ecca96a..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-var express = require('express');
-var router = express.Router();
-
-/* GET home page. */
-router.get('/', function(req, res, next) {
-  res.render('index', { title: 'Express' });
-});
-
-module.exports = router;
diff --git a/routes/users.js b/routes/users.js
deleted file mode 100644 (file)
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;
diff --git a/run-api.sh b/run-api.sh
new file mode 100755 (executable)
index 0000000..9ddc149
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+env=""
+if [[ ! -z "${PHS_ENV}" ]]; then
+   env=${PHS_ENV}
+fi
+
+if [ "$env" == "TEST" ]; then
+   echo "PHS API Test Stack"
+   npm install; npx pm2 start api.config.cjs --no-daemon --env TEST
+elif [ "$env" == "PRODUCTION" ]; then
+   echo "PHS API Production Stack"
+   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
+fi
diff --git a/src/config/dbConfig.json b/src/config/dbConfig.json
new file mode 100644 (file)
index 0000000..db102a1
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "user": "postgres",
+  "host": "localhost",
+  "port": "5432",
+  "password": "ab9cfz12",
+  "database": "postgres",
+  "migrations-dir": "./src/migrations",
+  "migrations-table": "pgmigrations",
+  "schema": "postgres",
+  "create-schema": true,
+  "migrations-schema": "phs"
+}
\ No newline at end of file
diff --git a/src/config/default.json b/src/config/default.json
new file mode 100644 (file)
index 0000000..a7e3ef9
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "keys": {
+    "secret": "SecretText",
+    "secret2fa": "67d0c783e3c18887bca5c9340917b7d8"
+  }
+}
\ No newline at end of file
diff --git a/src/config/secret_key_gen.js b/src/config/secret_key_gen.js
new file mode 100644 (file)
index 0000000..998ed7c
--- /dev/null
@@ -0,0 +1,11 @@
+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.
+const secretKey = crypto.randomBytes(16).toString('hex');
+logger.debug('key: ' + secretKey);
+
+const payload = { user_id: 682, verified_token: secretKey };
+const token = jwt.sign(payload, config.keys.secret2fa, { algorithm: 'HS256', expiresIn: '100y' });
+logger.debug('jwt token: ' + token);
diff --git a/src/controllers/git.controller.js b/src/controllers/git.controller.js
new file mode 100644 (file)
index 0000000..1937462
--- /dev/null
@@ -0,0 +1,60 @@
+const fs = require('fs');
+const sshConfig = {
+  host: 'localhost',
+  port: 22,
+  username: 'git',
+  privateKey: fs.readFileSync('/home/git/.ssh/git-ui'),
+};
+
+module.exports = {
+  createRepo: (req, res, next) => {
+      const { name, type, user } = req.body;
+      if (!name.match(/^[a-zA-Z0-9_-]+$/)) {
+        return res.status(400).json({ error: 'Invalid repository name' });
+      }
+      if (type === 'private' && !user) {
+        return res.status(400).json({ error: 'Username required for private repository' });
+      }
+
+      const conn = new Client();
+      conn.on('ready', () => {
+        const commands = type === 'private'
+          ? [
+            `mkdir /opt/git/${name}.git`,
+            `cd /opt/git/${name}.git`,
+            `git init --bare`,
+            `chown -R ${user}:git /opt/git/${name}.git`,
+            `chmod -R u+rwX,go-rwx /opt/git/${name}.git`,
+          ]
+          : [
+            `mkdir /opt/git/${name}.git`,
+            `cd /opt/git/${name}.git`,
+            `git init --bare --shared=group`,
+            `chown -R git:git /opt/git/${name}.git`,
+            `chmod -R g+rwX /opt/git/${name}.git`,
+            `find /opt/git/${name}.git -type d -exec chmod g+s {} \\;`,
+            `git config core.sharedRepository group`,
+          ];
+
+        conn.exec(`sudo -u git bash -c "${commands.join(' && ')}"`, (err, stream) => {
+          if (err) {
+            conn.end();
+            return res.status(500).json({ error: 'SSH command failed' });
+          }
+          let output = '';
+          stream.on('data', (data) => (output += data));
+          stream.stderr.on('data', (data) => (output += data));
+          stream.on('close', (code) => {
+            conn.end();
+            if (code === 0) {
+              res.json({ message: `Repository ${name}.git created successfully` });
+            } else {
+              res.status(500).json({ error: `Command failed: ${output}` });
+            }
+          });
+        });
+      }).on('error', (err) => {
+        res.status(500).json({ error: `SSH connection failed: ${err.message}` });
+      }).connect(sshConfig);
+    }
+};
\ No newline at end of file
diff --git a/src/db.js b/src/db.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/middleware/custom-startup.js b/src/middleware/custom-startup.js
new file mode 100644 (file)
index 0000000..74aafa9
--- /dev/null
@@ -0,0 +1,10 @@
+const phs_env = process.env.PHS_ENV;
+
+// Run once at startup
+async function runOnce() {
+    logger.debug('Running custom startup code once');
+    // Just return when not on PROD
+    if (phs_env !== 'PRODUCTION') return true
+}
+
+module.exports = runOnce;
\ No newline at end of file
diff --git a/src/middleware/loggerMiddleWare.js b/src/middleware/loggerMiddleWare.js
new file mode 100644 (file)
index 0000000..3fc1159
--- /dev/null
@@ -0,0 +1,57 @@
+const log4js = require("log4js");
+log4js.configure({
+  pm2: true,
+  appenders: {
+    access: {
+      type: "dateFile",
+      filename: "logs/access.log",
+      pattern: "-yyyy-MM-dd",
+      category: "http"
+    },
+    app: {
+      type: "file",
+      filename: "logs/app.log",
+      maxLogSize: 10485760,
+      numBackups: 3
+    },
+    out: {
+      type: 'stdout'
+    },
+    errorFile: {
+      type: "file",
+      filename: "logs/errors.log",
+      maxLogSize: 10485760
+    },
+    errors: {
+      type: "logLevelFilter",
+      level: "ERROR",
+      appender: "errorFile"
+    },
+    forms: {
+      type: "file",
+      filename: "logs/forms.log",
+      maxLogSize: 10485760,
+      numBackups: 3
+    }
+  },
+  categories: {
+    default: { "appenders": [ "app", "errors", 'out' ], "level": "DEBUG" },        
+    http: { "appenders": [ "access"], "level": "INFO" }
+  }
+});
+
+const logger = log4js.getLogger();
+
+module.exports = {
+  default: log4js.getLogger(),  
+  access: log4js.getLogger('access'),
+  app: log4js.getLogger('app'),
+  forms: log4js.getLogger('forms'),
+  http: log4js.getLogger('access'),
+  express: log4js.connectLogger(logger, {
+    level: 'debug', 
+    format: (req, res, format) => format(
+      ':method :url :status :response-time ms'
+    )
+  }),  
+};
diff --git a/src/middleware/multer.js b/src/middleware/multer.js
new file mode 100644 (file)
index 0000000..548fcc6
--- /dev/null
@@ -0,0 +1,12 @@
+const multer = require( 'multer' );
+
+const upload = multer({
+  limits: { fieldSize: 1024 * 1024 * 50 }
+});
+
+const passThrough = multer( {
+  limits: { fileSize: 1024 * 1024 * 50 },
+  storage: multer.memoryStorage()
+} );
+
+module.exports = { upload, passThrough };
\ No newline at end of file
diff --git a/src/middleware/passport.js b/src/middleware/passport.js
new file mode 100644 (file)
index 0000000..845bb71
--- /dev/null
@@ -0,0 +1,68 @@
+const User = require('../models/user.model');
+const JWTStrategy = require('passport-jwt').Strategy,
+  ExtractJwt = require('passport-jwt').ExtractJwt;
+
+const config = require('../config/default.json');
+
+// Hooks the JWT Strategy.
+function hookJWTStrategy(passport) {
+  logger.debug('hookJWTStrategy');
+  let options = {};
+
+  // options.secretOrKey = process.env['HASH_KEY'];
+  options.secretOrKey = config.keys.secret;
+  options.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
+  options.ignoreExpiration = true;
+
+  passport.use('jwt', new JWTStrategy(options, function (JWTPayload, callback) {
+    // return callback(null, {id: 1});
+    logger.debug(`JWT-JWTPayload: ${JSON.stringify(JWTPayload)}`);
+    /*return new User().findOne({ email: JWTPayload.email, is_active: true })*/
+    // return new User().findOne({ id: 1, is_active: true })
+    return phsdb.query(`select * from phs.users where id = $1;`, [1], { plain: true })
+      .then(async user => {
+        logger.debug('passport: ' + user);
+        if (!user?.id) {
+          return callback(null, false);
+        } else if (user?.id) {
+          // const roles = [...user.roles].map(r => r.name);
+          // if (roles.some(r => ['Administrator', 'ExecutiveManager', 'HR'].includes(r))) await rvdb.query('select crypto_key from rt2.users where id = $1;', [user.id], { plain: true }).then(cry => user.crypto_key = cry.crypto_key);
+          return callback(null, user);
+        } else {
+          return callback(null, false);
+        }
+      }).catch(error => callback(error, false));
+  }));
+
+  /*passport.use('jwt-contact', new JWTStrategy(options, function (JWTPayload, callback) {
+    logger.debug(`JWT-CONTACT-JWTPayload: ${JSON.stringify(JWTPayload)}`);
+    return db.contact().findOne({ id: JWTPayload.id, email: JWTPayload.email, is_active: true, is_deleted: false })
+      .then(async contact => {
+        // logger.debug('JWT-CONTACT: ' + JSON.stringify(contact));
+        if (!contact?.id) {
+          return callback(null, false);
+        } else if (contact?.id) {
+          return callback(null, contact);
+        } else {
+          return callback(null, false);
+        }
+      }).catch(error => callback(error, false));
+  }))
+
+  passport.use('it', new JWTStrategy(options, function (JWTPayload, callback) {
+    logger.debug(`JWT-IT-JWTPayload: ${JSON.stringify(JWTPayload)}`);
+    return db.it().user().findOne({ remote_id: JWTPayload.id, company_id: JWTPayload.company_id, is_active: true, is_deleted: false })
+      .then(async user => {
+        if (!user?.id) {
+          return callback(null, false);
+        } else if (user?.id) {
+          return callback(null, user);
+        } else {
+          return callback(null, false);
+        }
+      }).catch(error => callback(error, false));
+  }))*/
+  
+}
+
+module.exports = hookJWTStrategy;
\ No newline at end of file
diff --git a/src/middleware/routeHelpers.js b/src/middleware/routeHelpers.js
new file mode 100644 (file)
index 0000000..d78f0ab
--- /dev/null
@@ -0,0 +1,47 @@
+const validateAuth = function ( passport, context='jwt') {
+  logger.debug('validateAuth');
+  async function checkKey( req, res, next ) {
+    const { authorization, apikey:apiKey } = req.headers;
+    if (authorization) return passport.authenticate( context, { session:false } )(req, res, next);
+    // else if (!authorization && !apiKey) return passport.authenticate( context, { session: false } )(req, res, next);
+    else {
+      const key = await phsdb.query( 'select * from phs.api_keys where api_key = $1;', [apiKey], { plain:true } );
+      const user = await phsdb.query( 'select * from phs.users where id = 1;', [], { plain:true } );
+
+      logger.debug('helper: ' + user);
+
+      // const user = key ? await db.user().findOne( { id:key.user_id, is_deleted:false, is_active:true } ) : undefined;
+      /*const roles = user ? await phsdb.query(`
+          select r.*
+          from rt2.api_key_roles ur
+                   inner join rt2.roles r on r.id = ur.role_id
+          where ur.api_key_id = $1;`, [key.id] ) : undefined;*/
+      /*if (user && roles) {
+        user.roles = roles;
+      }*/
+      if (user) {
+        req.user = user;
+      }
+      next();
+    }
+  }
+
+  return checkKey;
+};
+
+const allowApi = function (apiKey, callback) {
+  logger.debug('allowApi');
+  function checkKey(req, res, next) {
+    logger.debug(req.params);
+    if (apiKey === req.params.apiKey) {
+      callback(req, res, next);
+    } else {
+      res.status(401).send({message: 'Your apiKey is not authorized!'});
+    }
+  }
+  return checkKey;
+};
+
+module.exports = { validateAuth, allowApi };
+
+/** @namespace params.apiKey */
diff --git a/src/models/model.js b/src/models/model.js
new file mode 100644 (file)
index 0000000..355741f
--- /dev/null
@@ -0,0 +1,215 @@
+// noinspection JSUnusedGlobalSymbols
+class Model {
+  constructor( props ) {
+    props && Object.keys( props ).forEach( c => {
+      this[c] = props[c];
+    } );
+    this.table = '';
+    this.defaultColumns = [];
+    this.updateExcludeColumns = ['id'];
+    this.prepend = '';
+    this.baseQuery = '';
+    this.baseListQuery = '';
+    this.defaultOrderBy = undefined;
+    this.instance = _props => new Model( _props );
+  };
+
+  whereClause = ( keys, prepend ) => keys?.length > 0 ? `where ${ keys.map( ( k, index ) => `${ prepend }.${ k } = $${ index + 1 }` ).join( ' and ' ) }` : '';
+
+  /**
+   * Build where clause for query
+   *
+   * @param {*} where
+   * @param {*} defaultColumns
+   * @returns
+   */
+  buildWhere = function ( where, defaultColumns ) {
+    const keys = [];
+    const values = [];
+    where && Object.keys( where ).forEach( k => {
+      if (defaultColumns.includes( k )) {
+        keys.push( k );
+        values.push( where[k] );
+      }
+    } );
+    return { keys, values };
+  };
+
+  /**
+   * Find one record
+   *
+   * @param {*} where
+   * @param {*} [excludes]
+   * @returns
+   */
+  findOne = function ( where, excludes = [] ) {
+    const self = this;
+    const { keys, values } = self.buildWhere( where, self.defaultColumns );
+    return phsdb.query( `
+  ${ self.baseQuery }
+  ${ self.whereClause( keys, self.prepend ) }
+  ${ self.groupBy ? self.groupBy : '' }
+  `, values, { plain:true } ).then( result => {
+      const found = self.instance( result );
+      excludes?.map( e => delete found[e] );
+      return found;
+    } );
+  };
+
+  /**
+   * Find one record
+   *
+   * @param {*} where
+   * @param {*} [excludes]
+   * @returns
+   */
+  findOneSimple = function ( where, excludes = [] ) {
+    const self = this;
+    const { keys, values } = self.buildWhere( where, self.defaultColumns );
+    return phsdb.query( `
+  ${ self.baseQuery }
+  ${ self.whereClause( keys, self.prepend ) }
+  ${ self.groupBy ? self.groupBy : '' }
+  `, values, { plain:true } ).then( result => {
+      excludes?.map( e => delete result[e] );
+      return result?.length > 0 ? result : null;
+    } );
+  };
+
+  /**
+   * @typedef {Object} orderBy
+   * @property {string} name
+   * @property {('asc', 'desc')} direction
+   */
+
+  getBaseListQuery = function () {
+    const self = this;
+    return `
+  ${ self.baseListQuery }
+  `;
+  };
+
+  /**
+   * Find many records
+   *
+   * @param {Object|undefined} [where]
+   * @param {string[]|undefined} [excludes]
+   * @param {orderBy|undefined} [orderBy] This can needs to be passed as an object with a name and optionally a direction. You can look at the vehicle controller and model for a good example.
+   * @returns
+   */
+  findMany = function ( where, excludes, orderBy ) {
+    const self = this;
+    const { keys, values } = self.buildWhere( where, self.defaultColumns );
+    return phsdb.query( `
+  ${ self.getBaseListQuery( self.whereClause( keys, self.prepend ) ) }
+  ${ self.whereClause( keys, self.prepend ) }
+  ${ self.groupBy ? self.groupBy : '' }
+  ${ orderBy ? `order by ${ orderBy.name } ${ orderBy.direction ?? 'asc' }` : self.defaultOrderBy ?? '' };
+  `, values ).then( async results => {
+      const res = [];
+      await results.forEach( result => {
+        const found = self.instance( result );
+        excludes?.map( e => delete found[e] );
+        return res.push( found );
+      } );
+      return res;
+    } );
+  };
+
+  /**
+   * Find many records
+   *
+   * @param {Object|undefined} [where]
+   * @param {string[]|undefined} [excludes]
+   * @param {orderBy|undefined} [orderBy] This can needs to be passed as an object with a name and optionally a direction. You can look at the vehicle controller and model for a good example.
+   * @returns
+   */
+  findManyLite = function ( where, excludes, orderBy ) {
+    const self = this;
+    const { keys, values } = self.buildWhere( where, self.defaultColumns );
+    return phsdb.query( `
+  ${ self.getBaseListQuery( self.whereClause( keys, self.prepend ) ) }
+  ${ self.whereClause( keys, self.prepend ) }
+  ${ self.groupBy ? self.groupBy : '' }
+  ${ orderBy ? `order by ${ orderBy.name } ${ orderBy.direction ?? 'asc' }` : self.defaultOrderBy ?? '' };
+  `, values ).then( async results => {
+      const res = [];
+      await results.forEach( result => {
+        const dataValues = {};
+        Object.keys( result ).map( k => dataValues[k] = result[k] );
+        excludes?.map( e => delete dataValues[e] );
+        return res.push( dataValues );
+      } );
+      return res;
+    } );
+  };
+
+  /**
+   * Update the record. Will update only the fields present
+   * in the params
+   *
+   * @param {*} params
+   * @returns
+   */
+  update = async function ( params ) {
+    const self = this;
+
+    // build list of update values
+    const { updates, values, position } = self.createUpdateFields( params );
+
+    // add id for where query
+    values.push( this.id );
+
+    // build query from values
+    const query = 'update ' + this.table + ' set ' + updates + ' where id=$' + position + ' returning *;';
+    logger.debug( `query: ${ query }` );
+
+    return await phsdb.query( query, values, { plain:true } )
+      .then( () => {
+        return self.findOne( { id:this.id } );
+      } );
+  };
+
+  toJSON = function () {
+    const {
+      defaultColumns,
+      prepend,
+      baseQuery,
+      baseListQuery,
+      table,
+      updateExcludeColumns,
+      agingDownloadQuery,
+      baseListExtendedQuery,
+      baseListQueryForTech,
+      instance,
+      db,
+      defaultOrderBy,
+      excludes,
+      ...rest
+    } = this;
+    return rest;
+  };
+
+  /**
+   * Create update fields string and values based on
+   * the params and the column configuration
+   */
+  createUpdateFields( params ) {
+    let position = 1;
+    let values = [];
+    let updates = '';
+    for (const column of this.defaultColumns) {
+      if (params.hasOwnProperty( column ) && !this.updateExcludeColumns.includes( column )) {
+        const value = params[column];
+        this[column] = value;
+        values.push( value );
+        updates += ((updates ? ',' : '') + column + '=$' + position++);
+      }
+    }
+
+    return { updates, values, position };
+  }
+
+}
+
+module.exports = Model;
\ No newline at end of file
diff --git a/src/phsdb.js b/src/phsdb.js
new file mode 100644 (file)
index 0000000..58f9c5a
--- /dev/null
@@ -0,0 +1,90 @@
+// noinspection RequiredAttributes
+
+const { Pool } = require('pg');
+const connectionString = 'postgres://postgres:ab9cfz12@clr-db:5432/postgres';
+const pgp = require('pg-promise')();
+
+const pool = new Pool({
+  connectionString,
+  max: 50
+});
+
+const json_slimify = (key, value) => {
+  if (key === "data") return '<data>'
+  if (key === "photo_data") return '<photo_data>'
+  if (key === "photo_data_tn") return '<photo_data_tn>'  
+  if (typeof value === 'string' && value.length > 100) {
+    return value.substring(0, 100) + '...';
+  } else {
+    return 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.
+    const formattedQuery = pgp.as.format(text, values);
+    logger.debug(
+      `[Executing Query] ${formattedQuery} -- With values: ${JSON.stringify(
+        values,json_slimify
+      )}`,
+    );
+  } catch (err) {
+    logger.error('Error formatting query:', err);
+  }
+}
+
+module.exports.query = async (text, values, options={}) => {
+  if (!text) return null
+  if (!values) values = []
+  
+  log_query(text, values, options)
+  
+  const results = await pool.query(text, values)
+  if (results && results.rows) {    
+    return options?.plain ? results.rows[0] : results.rows;
+  } else {
+    return null
+  }
+}
+
+module.exports.get_pool = () => {
+  return pool
+}
+
+module.exports.cleanup = async () => {
+  await pool.end()
+}
+
+module.exports.get_client = async () => {
+  // noinspection JSCheckFunctionSignatures
+  return await pool.connect();
+}
+
+module.exports.client_release = async (client) => {
+  await client.release()
+}
+
+module.exports.client_query = async (client, text, values, options={}) => {
+  if (!text) return null
+  if (!values) values = []
+
+  log_query(text, values, options)
+
+  const results = await client.query(text, values)
+  if (results && results.rows) {    
+    return options?.plain ? results.rows[0] : results.rows;
+  } else {
+    return null
+  }
+}
+
+module.exports.get_pool_stats = () => {
+  return {
+    total: pool.totalCount,
+    idle: pool.idleCount,
+    waiting: pool.waitingCount
+  }
+}
+
diff --git a/src/routes/git.routes.js b/src/routes/git.routes.js
new file mode 100644 (file)
index 0000000..4734d9d
--- /dev/null
@@ -0,0 +1,11 @@
+const express = require('express');
+const router = express.Router();
+const { validateAuth } = require('../middleware/routeHelpers');
+const gitController = require('../controllers/git.controller');
+
+module.exports = (passport) => {
+  router.use( validateAuth( passport ) );
+  router.post('/create-repo', gitController.createRepo);
+
+  return router;
+};
\ No newline at end of file
diff --git a/src/routes/index.js b/src/routes/index.js
new file mode 100644 (file)
index 0000000..6da96af
--- /dev/null
@@ -0,0 +1,16 @@
+const express = require('express');
+const router = express.Router();
+
+module.exports.APIRoutes = ( passport ) => {
+  /* GET home page. */
+  router.get('/', function(req, res, next) {
+    res.render('index', { title: 'Express' });
+  });
+
+  router.use( '/git', require('./git.routes')(passport) );
+  return router;
+}
+
+
+
+module.exports = router;
diff --git a/src/routes/users.js b/src/routes/users.js
new file mode 100644 (file)
index 0000000..623e430
--- /dev/null
@@ -0,0 +1,9 @@
+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;
diff --git a/src/swagger.json b/src/swagger.json
new file mode 100644 (file)
index 0000000..8c330fb
--- /dev/null
@@ -0,0 +1,2587 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "description": "API for managing units, inspection checks, and related entities",
+    "version": "1.0.0",
+    "title": "CES API",
+    "contact": {
+      "email": "abc@gmail.com"
+    },
+    "license": {
+      "name": "Apache 2.0",
+      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
+    }
+  },
+  "schemes": ["https", "http"],
+  "host": "api-phasecustomsoft.com",
+  "basePath": "/",
+  "securityDefinitions": {
+    "Bearer": {
+      "type": "apiKey",
+      "name": "Authorization",
+      "in": "header"
+    }
+  },
+  "security": [{"Bearer": []}],
+  "paths": {
+    "/base_inspection_check/": {
+      "post": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Create a new base inspection check",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Base inspection check object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/{inspection_check_id}": {
+      "get": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Get base inspection check by ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "ID of base inspection check to return",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Base inspection check not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Delete base inspection check",
+        "description": "",
+        "parameters": [
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "ID of base inspection check to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Base inspection check not found"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/dot": {
+      "get": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Get DOT-related base inspection checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/BaseInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/static": {
+      "get": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Get static base inspection checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/BaseInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/{inspection_check_id}/photo-requirements": {
+      "put": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Update photo requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "ID of base inspection check to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Photo requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "photo": {
+                  "type": "boolean"
+                },
+                "photo_required": {
+                  "type": "boolean"
+                },
+                "photo_only": {
+                  "type": "boolean"
+                },
+                "photo_count": {
+                  "type": "integer"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Base inspection check not found"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/{inspection_check_id}/details-requirements": {
+      "put": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Update details requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "ID of base inspection check to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Details requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "details": {
+                  "type": "boolean"
+                },
+                "details_required": {
+                  "type": "boolean"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Base inspection check not found"
+          }
+        }
+      }
+    },
+    "/base_inspection_check/{inspection_check_id}/sort-order": {
+      "put": {
+        "tags": ["Base Inspection Check"],
+        "summary": "Update sort order",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "ID of base inspection check to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Sort order object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "sort_order": {
+                  "type": "number",
+                  "format": "double"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/BaseInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Base inspection check not found"
+          }
+        }
+      }
+    },
+    "/user/": {
+      "post": {
+        "tags": ["User"],
+        "summary": "Create a new user",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "User object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/user/by-employee-number/{employee_number}": {
+      "get": {
+        "tags": ["User"],
+        "summary": "Get user by employee number",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "employee_number",
+            "in": "path",
+            "description": "Employee number of user to return",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          },
+          "400": {
+            "description": "Invalid employee number supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      }
+    },
+    "/user/active": {
+      "get": {
+        "tags": ["User"],
+        "summary": "Get active users",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/User"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/user/{id}/deactivate": {
+      "put": {
+        "tags": ["User"],
+        "summary": "Deactivate user",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of user to deactivate",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Deactivation details",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "deactivatedById": {
+                  "type": "integer"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      }
+    },
+    "/user/{id}/reactivate": {
+      "put": {
+        "tags": ["User"],
+        "summary": "Reactivate user",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of user to reactivate",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      }
+    },
+    "/user/{id}": {
+      "put": {
+        "tags": ["User"],
+        "summary": "Update user",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of user to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "User object to update",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/User"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": ["User"],
+        "summary": "Delete user",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of user to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      }
+    },
+    "/unit_category/": {
+      "post": {
+        "tags": ["Unit Category"],
+        "summary": "Create a new unit category",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Unit category object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UnitCategory"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/UnitCategory"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "get": {
+        "tags": ["Unit Category"],
+        "summary": "Get all unit categories",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitCategory"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category/by-name/{name}": {
+      "get": {
+        "tags": ["Unit Category"],
+        "summary": "Get unit category by name",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "name",
+            "in": "path",
+            "description": "Name of unit category to return",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategory"
+            }
+          },
+          "400": {
+            "description": "Invalid name supplied"
+          },
+          "404": {
+            "description": "Unit category not found"
+          }
+        }
+      }
+    },
+    "/unit_category/name-exists/{name}": {
+      "get": {
+        "tags": ["Unit Category"],
+        "summary": "Check if unit category name exists",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "name",
+            "in": "path",
+            "description": "Name to check",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "object",
+              "properties": {
+                "exists": {
+                  "type": "boolean"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category/{id}/name": {
+      "put": {
+        "tags": ["Unit Category"],
+        "summary": "Update unit category name",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of unit category to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "New name object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "name": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategory"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit category not found"
+          }
+        }
+      }
+    },
+    "/unit_category/{id}": {
+      "delete": {
+        "tags": ["Unit Category"],
+        "summary": "Delete unit category",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of unit category to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Unit category not found"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/": {
+      "post": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Create a new unit type inspection check",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Unit type inspection check object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/{unit_type_id}/{inspection_check_id}": {
+      "get": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Get unit type inspection check by IDs",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid IDs supplied"
+          },
+          "404": {
+            "description": "Unit type inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/by-unit-type/{unit_type_id}": {
+      "get": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Get checks by unit type",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitTypeInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid unit_type_id supplied"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/dot": {
+      "get": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Get DOT-related checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitTypeInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/static": {
+      "get": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Get static checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitTypeInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/{unit_type_id}/{inspection_check_id}/photo-requirements": {
+      "put": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Update photo requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Photo requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "photo": {
+                  "type": "boolean"
+                },
+                "photo_required": {
+                  "type": "boolean"
+                },
+                "photo_only": {
+                  "type": "boolean"
+                },
+                "photo_count": {
+                  "type": "integer"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit type inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/{unit_type_id}/{inspection_check_id}/details-requirements": {
+      "put": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Update details requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Details requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "details": {
+                  "type": "boolean"
+                },
+                "details_required": {
+                  "type": "boolean"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit type inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_type_inspection_check/{unit_type_id}/{inspection_check_id}/sort-order": {
+      "put": {
+        "tags": ["Unit Type Inspection Check"],
+        "summary": "Update sort order",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Sort order object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "sort_order": {
+                  "type": "number",
+                  "format": "double"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitTypeInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit type inspection check not found"
+          }
+        }
+      }
+    },
+    "/api_key/": {
+      "post": {
+        "tags": ["API Key"],
+        "summary": "Create a new API key",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "API key object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/api_key/{id}": {
+      "get": {
+        "tags": ["API Key"],
+        "summary": "Get API key by ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of API key to return",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "API key not found"
+          }
+        }
+      },
+      "put": {
+        "tags": ["API Key"],
+        "summary": "Update API key",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of API key to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "API key object to update",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "API key not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": ["API Key"],
+        "summary": "Delete API key",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of API key to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "API key not found"
+          }
+        }
+      }
+    },
+    "/api_key/by-key/{apiKey}": {
+      "get": {
+        "tags": ["API Key"],
+        "summary": "Get API key by value",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "apiKey",
+            "in": "path",
+            "description": "API key value to return",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/ApiKey"
+            }
+          },
+          "400": {
+            "description": "Invalid API key supplied"
+          },
+          "404": {
+            "description": "API key not found"
+          }
+        }
+      }
+    },
+    "/unit/": {
+      "get": {
+        "tags": ["Unit"],
+        "summary": "Get all units",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Unit"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "post": {
+        "tags": ["Unit"],
+        "summary": "Create a new unit",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Unit object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/Unit"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/Unit"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit/by-unit-type/{unit_type_id}": {
+      "get": {
+        "tags": ["Unit"],
+        "summary": "Get units by unit type ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_type_id",
+            "in": "path",
+            "description": "Unit type ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Unit"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid unit_type_id supplied"
+          }
+        }
+      }
+    },
+    "/unit/by-unit-category/{unit_category_id}": {
+      "get": {
+        "tags": ["Unit"],
+        "summary": "Get units by unit category ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Unit"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid unit_category_id supplied"
+          }
+        }
+      }
+    },
+    "/unit/inspections/": {
+      "get": {
+        "tags": ["Unit"],
+        "summary": "Get inspections by lookup code",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "query",
+            "description": "Lookup code",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/InspectionCheck"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/": {
+      "post": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Create a new unit category inspection check",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Unit category inspection check object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/{unit_category_id}/{inspection_check_id}": {
+      "get": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Get unit category inspection check by IDs",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid IDs supplied"
+          },
+          "404": {
+            "description": "Unit category inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/by-unit-category/{unit_category_id}": {
+      "get": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Get checks by unit category",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitCategoryInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid unit_category_id supplied"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/dot": {
+      "get": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Get DOT-related checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitCategoryInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/static": {
+      "get": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Get static checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UnitCategoryInspection"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/{unit_category_id}/{inspection_check_id}/photo-requirements": {
+      "put": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Update photo requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Photo requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "photo": {
+                  "type": "boolean"
+                },
+                "photo_required": {
+                  "type": "boolean"
+                },
+                "photo_only": {
+                  "type": "boolean"
+                },
+                "photo_count": {
+                  "type": "integer"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit category inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/{unit_category_id}/{inspection_check_id}/details-requirements": {
+      "put": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Update details requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Details requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "details": {
+                  "type": "boolean"
+                },
+                "details_required": {
+                  "type": "boolean"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit category inspection check not found"
+          }
+        }
+      }
+    },
+    "/unit_category_inspection_check/{unit_category_id}/{inspection_check_id}/sort-order": {
+      "put": {
+        "tags": ["Unit Category Inspection Check"],
+        "summary": "Update sort order",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_category_id",
+            "in": "path",
+            "description": "Unit category ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "name": "inspection_check_id",
+            "in": "path",
+            "description": "Inspection check ID",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Sort order object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "sort_order": {
+                  "type": "number",
+                  "format": "double"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/UnitCategoryInspection"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Unit category inspection check not found"
+          }
+        }
+      }
+    },
+    "/inspection_check/": {
+      "post": {
+        "tags": ["Inspection Check"],
+        "summary": "Create a new inspection check",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Inspection check object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/InspectionCheck"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/InspectionCheck"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "get": {
+        "tags": ["Inspection Check"],
+        "summary": "Get all inspection checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/InspectionCheck"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/inspection_check/by-name/{name}": {
+      "get": {
+        "tags": ["Inspection Check"],
+        "summary": "Get inspection check by name",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "name",
+            "in": "path",
+            "description": "Name of inspection check to return",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/InspectionCheck"
+            }
+          },
+          "400": {
+            "description": "Invalid name supplied"
+          },
+          "404": {
+            "description": "Inspection check not found"
+          }
+        }
+      }
+    },
+    "/inspection_check/dot": {
+      "get": {
+        "tags": ["Inspection Check"],
+        "summary": "Get DOT-related inspection checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/InspectionCheck"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/inspection_check/static": {
+      "get": {
+        "tags": ["Inspection Check"],
+        "summary": "Get static inspection checks",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/InspectionCheck"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/inspection_check/{id}/photo-requirements": {
+      "put": {
+        "tags": ["Inspection Check"],
+        "summary": "Update photo requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of inspection check to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Photo requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "photo": {
+                  "type": "boolean"
+                },
+                "photo_required": {
+                  "type": "boolean"
+                },
+                "photo_only": {
+                  "type": "boolean"
+                },
+                "photo_count": {
+                  "type": "integer"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/InspectionCheck"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Inspection check not found"
+          }
+        }
+      }
+    },
+    "/inspection_check/{id}/details-requirements": {
+      "put": {
+        "tags": ["Inspection Check"],
+        "summary": "Update details requirements",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of inspection check to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Details requirements object",
+            "required": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "details": {
+                  "type": "boolean"
+                },
+                "details_required": {
+                  "type": "boolean"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/InspectionCheck"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Inspection check not found"
+          }
+        }
+      }
+    },
+    "/inspection_check/{id}": {
+      "delete": {
+        "tags": ["Inspection Check"],
+        "summary": "Delete inspection check",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of inspection check to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Inspection check not found"
+          }
+        }
+      }
+    },
+    "/form/": {
+      "post": {
+        "tags": ["Form"],
+        "summary": "Create a new form",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Form object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/Form"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/Form"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "get": {
+        "tags": ["Form"],
+        "summary": "Get all forms",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Form"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/form/{id}": {
+      "get": {
+        "tags": ["Form"],
+        "summary": "Get form by ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form to return",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/Form"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Form not found"
+          }
+        }
+      },
+      "put": {
+        "tags": ["Form"],
+        "summary": "Update form",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Form object to update",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/Form"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/Form"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Form not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": ["Form"],
+        "summary": "Delete form",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Form not found"
+          }
+        }
+      }
+    },
+    "/form/by-unit/{unit_id}": {
+      "get": {
+        "tags": ["Form"],
+        "summary": "Get forms by unit ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "unit_id",
+            "in": "path",
+            "description": "Unit ID to filter forms",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Form"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid unit_id supplied"
+          }
+        }
+      }
+    },
+    "/form/by-user/{user_id}": {
+      "get": {
+        "tags": ["Form"],
+        "summary": "Get forms by user ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "user_id",
+            "in": "path",
+            "description": "User ID to filter forms",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Form"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid user_id supplied"
+          }
+        }
+      }
+    },
+    "/form_attachment/": {
+      "post": {
+        "tags": ["Form Attachment"],
+        "summary": "Create a new form attachment",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Form attachment object",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/FormAttachment"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/FormAttachment"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "get": {
+        "tags": ["Form Attachment"],
+        "summary": "Get all form attachments",
+        "description": "",
+        "produces": ["application/json"],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/FormAttachment"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      }
+    },
+    "/form_attachment/{id}": {
+      "get": {
+        "tags": ["Form Attachment"],
+        "summary": "Get form attachment by ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form attachment to return",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/FormAttachment"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Form attachment not found"
+          }
+        }
+      },
+      "put": {
+        "tags": ["Form Attachment"],
+        "summary": "Update form attachment",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form attachment to update",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "body",
+            "description": "Form attachment object to update",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/FormAttachment"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "$ref": "#/definitions/FormAttachment"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "404": {
+            "description": "Form attachment not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": ["Form Attachment"],
+        "summary": "Delete form attachment",
+        "description": "",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "ID of form attachment to delete",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Deleted"
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Form attachment not found"
+          }
+        }
+      }
+    },
+    "/form_attachment/by-form/{form_id}": {
+      "get": {
+        "tags": ["Form Attachment"],
+        "summary": "Get form attachments by form ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "form_id",
+            "in": "path",
+            "description": "Form ID to filter attachments",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/FormAttachment"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid form_id supplied"
+          }
+        }
+      }
+    },
+    "/form_attachment/by-creator/{created_by_id}": {
+      "get": {
+        "tags": ["Form Attachment"],
+        "summary": "Get form attachments by creator ID",
+        "description": "",
+        "produces": ["application/json"],
+        "parameters": [
+          {
+            "name": "created_by_id",
+            "in": "path",
+            "description": "User ID who created the attachments",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful operation",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/FormAttachment"
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid created_by_id supplied"
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "BaseInspection": {
+      "type": "object",
+      "properties": {
+        "inspection_check_id": {
+          "type": "integer"
+        },
+        "sort_order": {
+          "type": "number",
+          "format": "double"
+        },
+        "dot": {
+          "type": "boolean"
+        },
+        "photo": {
+          "type": "boolean"
+        },
+        "details": {
+          "type": "boolean"
+        },
+        "details_required": {
+          "type": "boolean"
+        },
+        "photo_required": {
+          "type": "boolean"
+        },
+        "photo_only": {
+          "type": "boolean"
+        },
+        "photo_count": {
+          "type": "integer"
+        },
+        "is_static": {
+          "type": "boolean"
+        }
+      }
+    },
+    "User": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "name": {
+          "type": "string"
+        },
+        "employee_number": {
+          "type": "string"
+        },
+        "password": {
+          "type": "string"
+        },
+        "is_active": {
+          "type": "boolean"
+        },
+        "deactivated_at": {
+          "type": "string",
+          "format": "date-time"
+        },
+        "deactivated_by_id": {
+          "type": "integer"
+        }
+      }
+    },
+    "UnitCategory": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "name": {
+          "type": "string"
+        }
+      }
+    },
+    "UnitTypeInspection": {
+      "type": "object",
+      "properties": {
+        "unit_type_id": {
+          "type": "integer"
+        },
+        "inspection_check_id": {
+          "type": "integer"
+        },
+        "sort_order": {
+          "type": "number",
+          "format": "double"
+        },
+        "dot": {
+          "type": "boolean"
+        },
+        "photo": {
+          "type": "boolean"
+        },
+        "details": {
+          "type": "boolean"
+        },
+        "details_required": {
+          "type": "boolean"
+        },
+        "photo_required": {
+          "type": "boolean"
+        },
+        "photo_only": {
+          "type": "boolean"
+        },
+        "photo_count": {
+          "type": "integer"
+        },
+        "is_static": {
+          "type": "boolean"
+        }
+      }
+    },
+    "Unit": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "lookup_code": {
+          "type": "string"
+        },
+        "unit_id": {
+          "type": "string"
+        },
+        "unit_category_id": {
+          "type": "integer"
+        },
+        "unit_type_id": {
+          "type": "integer"
+        }
+      }
+    },
+    "UnitCategoryInspection": {
+      "type": "object",
+      "properties": {
+        "unit_category_id": {
+          "type": "integer"
+        },
+        "inspection_check_id": {
+          "type": "integer"
+        },
+        "sort_order": {
+          "type": "number",
+          "format": "double"
+        },
+        "dot": {
+          "type": "boolean"
+        },
+        "photo": {
+          "type": "boolean"
+        },
+        "details": {
+          "type": "boolean"
+        },
+        "details_required": {
+          "type": "boolean"
+        },
+        "photo_required": {
+          "type": "boolean"
+        },
+        "photo_only": {
+          "type": "boolean"
+        },
+        "photo_count": {
+          "type": "integer"
+        },
+        "is_static": {
+          "type": "boolean"
+        }
+      }
+    },
+    "ApiKey": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "api_key": {
+          "type": "string"
+        },
+        "user_id": {
+          "type": "integer"
+        }
+      }
+    },
+    "InspectionCheck": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "name": {
+          "type": "string"
+        },
+        "dot": {
+          "type": "boolean"
+        },
+        "photo": {
+          "type": "boolean"
+        },
+        "details": {
+          "type": "boolean"
+        },
+        "details_required": {
+          "type": "boolean"
+        },
+        "photo_required": {
+          "type": "boolean"
+        },
+        "photo_only": {
+          "type": "boolean"
+        },
+        "photo_count": {
+          "type": "integer"
+        },
+        "is_static": {
+          "type": "boolean"
+        }
+      }
+    },
+    "UnitType": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "name": {
+          "type": "string"
+        },
+        "unit_category_id": {
+          "type": "integer"
+        }
+      }
+    },
+    "Form": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "unit_id": {
+          "type": "integer"
+        },
+        "user_id": {
+          "type": "integer"
+        },
+        "form_data": {
+          "type": "object"
+        },
+        "created_at": {
+          "type": "string",
+          "format": "date-time"
+        }
+      }
+    },
+    "FormAttachment": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "form_id": {
+          "type": "integer"
+        },
+        "file_data": {
+          "type": "string",
+          "format": "byte"
+        },
+        "file_type": {
+          "type": "string"
+        },
+        "created_by_id": {
+          "type": "integer"
+        },
+        "created_at": {
+          "type": "string",
+          "format": "date-time"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file