From: charleswrayjr Date: Fri, 12 Sep 2025 04:16:23 +0000 (-0500) Subject: Adding role models, routes, and controllers. X-Git-Url: https://git.phasecustomsoft.com/static/git-favicon.png?a=commitdiff_plain;h=da6a4d01d02b729e71e3afbb89626f6fb14affb2;p=phs-api.git Adding role models, routes, and controllers. --- diff --git a/src/controllers/role.controller.js b/src/controllers/role.controller.js new file mode 100644 index 0000000..3050ac1 --- /dev/null +++ b/src/controllers/role.controller.js @@ -0,0 +1,128 @@ +/** + * @file Role controller for handling role-related API requests + */ + +const db = require( '../models' ); +const createError = require( 'http-errors' ); + +/** + * Role controller + * @type {Object} + */ +module.exports = { + /** + * Create a new role + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async create( req, res, next ) { + try { + const role_data = req.body; + const role = await db.role.create( role_data ); + res.json( role ); + } catch (error) { + logger.error( `Create role error: ${ error.message }` ); + next( createError( error.status || 409, error.message ) ); + } + }, + + /** + * Find role by name + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_name( req, res, next ) { + try { + const { name } = req.params; + const role = await db.role.find_by_name( name ); + if (!role) return next( createError( 404, 'Role not found' ) ); + res.json( role ); + } catch (error) { + logger.error( `Find role by name error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Find one role by ID + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_one( req, res, next ) { + try { + const { id } = req.params; + const role = await db.role.find_one( { id:parseInt( id ) } ); + if (!role) return next( createError( 404, 'Role not found' ) ); + res.json( role ); + } catch (error) { + logger.error( `Find role error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Find many roles + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_many( req, res, next ) { + try { + const { limit = '100', offset = '0', ...where } = req.query; + const roles = await db.role.find_many( where, [], null, parseInt( limit ), parseInt( offset ) ); + res.json( roles ); + } catch (error) { + logger.error( `Find many roles error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Update a role + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async update( req, res, next ) { + try { + const { id } = req.params; + const role_data = req.body; + const role = await db.role.instance().find_one( { id:parseInt( id ) } ); + if (!role) return next( createError( 404, 'Role not found' ) ); + const updated_role = await role.update( role_data ); + res.json( updated_role ); + } catch (error) { + logger.error( `Update role error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Soft delete a role + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async soft_delete( req, res, next ) { + try { + const { id } = req.params; + const { deleted_by_id } = req.body; + const role = await db.role.instance().find_one( { id:parseInt( id ) } ); + if (!role) return next( createError( 404, 'Role not found' ) ); + const deleted_role = await role.soft_delete( deleted_by_id ); + res.json( deleted_role ); + } catch (error) { + logger.error( `Soft delete role error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + } +}; \ No newline at end of file diff --git a/src/controllers/user_roles.controller.js b/src/controllers/user_roles.controller.js new file mode 100644 index 0000000..be3e2cb --- /dev/null +++ b/src/controllers/user_roles.controller.js @@ -0,0 +1,87 @@ +/** + * @file User roles controller for handling user role-related API requests + */ + +const db = require( '../models' ); +const createError = require( 'http-errors' ); + +/** + * User roles controller + * @type {Object} + */ +module.exports = { + /** + * Add a user-role relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async add_relation( req, res, next ) { + try { + const { user_id, role_id } = req.body; + const relation = await db.user_roles.add_relation( user_id, role_id ); + res.json( relation ); + } catch (error) { + logger.error( `Add user-role relation error: ${ error.message }` ); + next( createError( error.status || 409, error.message ) ); + } + }, + + /** + * Remove a user-role relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async remove_relation( req, res, next ) { + try { + const { role_id, user_id } = req.body; + const relation = await db.user_roles.remove_relation( role_id, user_id ); + if (!relation) return next( createError( 404, 'Relation not found' ) ); + res.json( relation ); + } catch (error) { + logger.error( `Remove user-role relation error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Find user-role relation by IDs + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_ids( req, res, next ) { + try { + const { user_id, role_id } = req.params; + const relation = await db.user_roles.find_by_ids( parseInt( user_id ), parseInt( role_id ) ); + if (!relation) return next( createError( 404, 'Relation not found' ) ); + res.json( relation ); + } catch (error) { + logger.error( `Find user-role by IDs error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Find user-role relations by user ID + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_user_id( req, res, next ) { + try { + const { user_id } = req.params; + const { limit = '100', offset = '0' } = req.query; + const relations = await db.user_roles.find_by_user_id( parseInt( user_id ), [], null, parseInt( limit ), parseInt( offset ) ); + res.json( relations ); + } catch (error) { + logger.error( `Find user-roles by user ID error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + } +}; \ No newline at end of file diff --git a/src/models/index.js b/src/models/index.js index 39cd411..d6d6f72 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -49,6 +49,16 @@ const get_user_addresses_model = () => get_model( 'user_addresses' ); */ const get_authentication_model = () => get_model( 'authentication' ); +/** + * @returns {Function} Role model class + */ +const get_role_model = () => get_model( 'role' ); + +/** + * @returns {Function} User roles model class + */ +const get_user_roles_model = () => get_model( 'user_roles' ); + /** * Unified database interface * @type {Object} @@ -620,6 +630,143 @@ const db = { * @returns {Authentication} Authentication instance */ instance:() => new (get_authentication_model())() + }, + role:{ + /** + * Create a new role + * @param {Object} role_data - Role data + * @returns {Promise} Created role instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + async create( role_data ) { + try { + return await get_role_model().create( role_data ); + } catch (error) { + logger.error( `Failed to create role: ${ error.message }` ); + throw error; + } + }, + /** + * Find role by name + * @param {string} name - Role name to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Role instance or null + */ + async find_by_name( name, excludes ) { + try { + return await get_role_model().find_by_name( name, excludes ); + } catch (error) { + logger.error( `Failed to find role by name: ${ error.message }` ); + throw error; + } + }, + /** + * Find one role + * @param {Object} where - Conditions + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Role instance or null + */ + async find_one( where, excludes ) { + try { + return await new (get_role_model())().find_one( where, excludes ); + } catch (error) { + logger.error( `Failed to find one role: ${ error.message }` ); + throw error; + } + }, + /** + * Find many roles + * @param {Object} [where] - Conditions + * @param {string[]} [excludes] - Fields to exclude + * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration + * @param {number} [limit] - Maximum number of records + * @param {number} [offset] - Number of records to skip + * @returns {Promise} Array of roles + */ + async find_many( where, excludes, order_by, limit, offset ) { + try { + return await new (get_role_model())().find_many( where, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find many roles: ${ error.message }` ); + throw error; + } + }, + /** + * Create a new Role instance + * @returns {Role} Role instance + */ + instance:() => new (get_role_model())() + }, + user_roles:{ + /** + * Add a user-role relation + * @param {number|string} user_id - User ID + * @param {number|string} role_id - Role ID + * @returns {Promise} Created relation instance + * @throws {ValidationError} If IDs are invalid + * @throws {FailedToCreateError} If creation fails + */ + async add_relation( user_id, role_id ) { + try { + return await get_user_roles_model().add_relation( user_id, role_id ); + } catch (error) { + logger.error( `Failed to add user-role relation: ${ error.message }` ); + throw error; + } + }, + /** + * Remove a user-role relation + * @param {number|string} role_id - Role ID + * @param {number|string|undefined} [user_id] - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {ValidationError} If IDs are invalid + */ + async remove_relation( role_id, user_id ) { + try { + return await get_user_roles_model().remove_relation( role_id, user_id ); + } catch (error) { + logger.error( `Failed to remove user-role relation: ${ error.message }` ); + throw error; + } + }, + /** + * Find user-role relation by IDs + * @param {number|string} user_id - User ID + * @param {number|string} role_id - Role ID + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Relation instance or null + */ + async find_by_ids( user_id, role_id, excludes ) { + try { + return await get_user_roles_model().find_by_ids( user_id, role_id, excludes ); + } catch (error) { + logger.error( `Failed to find user-role by IDs: ${ error.message }` ); + throw error; + } + }, + /** + * Find user-role relations by user ID + * @param {number|string} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude + * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration + * @param {number} [limit] - Maximum number of records + * @param {number} [offset] - Number of records to skip + * @returns {Promise} Array of relations + */ + async find_by_user_id( user_id, excludes, order_by, limit, offset ) { + try { + return await get_user_roles_model().find_by_user_id( user_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find user-roles by user ID: ${ error.message }` ); + throw error; + } + }, + /** + * Create a new UserRole instance + * @returns {UserRole} UserRole instance + */ + instance:() => new (get_user_roles_model())() } }; diff --git a/src/models/role.model.js b/src/models/role.model.js new file mode 100644 index 0000000..94a4753 --- /dev/null +++ b/src/models/role.model.js @@ -0,0 +1,109 @@ +/** + * @file Role model for phase.roles table + */ + +const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} Role + * @property {number} id - Role ID (primary key) + * @property {string} name - Role name + * @property {number} created_by_id - ID of user who created this role + * @property {Date} created_at - Creation timestamp + * @property {boolean} is_deleted - Soft delete flag + * @property {number|null} deleted_by_id - ID of user who deleted this role + * @property {Date|null} deleted_at - Deletion timestamp + */ + +/** + * Role model class + * @extends Model + */ +class Role extends Model { + /** + * Create a Role instance + * @param {Partial} [props] - Role properties + */ + constructor( props ) { + super( props ); + this.table = 'phase.roles'; + this.prepend = 'r'; + this.default_columns = [ + 'id', 'name', 'created_by_id', 'created_at', + 'is_deleted', 'deleted_by_id', 'deleted_at' + ]; + this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id']; + this.base_query = ` + SELECT r.id, + r.name, + r.created_by_id, + r.created_at, + r.is_deleted, + r.deleted_by_id, + r.deleted_at + FROM phase.roles r + WHERE r.is_deleted = false + `; + this.base_list_query = ` + SELECT r.id, r.name, r.created_by_id, r.created_at + FROM phase.roles r + WHERE r.is_deleted = false + `; + this.default_order_by = 'ORDER BY r.name ASC'; + this.instance = _props => new Role( _props ); + } + + /** + * Create a new role + * @param {Omit} role_data - Role data + * @returns {Promise} Created role instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + static async create( role_data ) { + const { name, created_by_id } = role_data; + if (!name || !created_by_id) { + throw new ValidationError( 'Missing required fields: name, created_by_id' ); + } + const query_str = ` + INSERT INTO phase.roles (name, created_by_id) + VALUES ($1, $2) + RETURNING *; + `; + const values = [name, created_by_id]; + const result = await phsdb.query( query_str, values, { plain:true } ); + if (!result) throw new FailedToCreateError( 'Failed to create role' ); + return new Role( result ); + } + + /** + * Find role by name + * @param {string} name - Role name to search for + * @param {string[]} [excludes] - Fields to exclude from result + * @returns {Promise} Role instance or null + */ + static async find_by_name( name, excludes = [] ) { + return await new Role().find_one( { name }, excludes ); + } + + /** + * Soft delete role + * @param {number|string} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated role instance + * @throws {ValidationError} If deleted_by_id is invalid + * @throws {NotFoundError} If record not found + */ + async soft_delete( deleted_by_id ) { + const deleted_by_id_int = parseInt( deleted_by_id, 10 ); + if (isNaN( deleted_by_id_int )) { + throw new ValidationError( 'deleted_by_id must be a valid integer' ); + } + return await this.update( { + is_deleted:true, + deleted_at:new Date().toISOString(), + deleted_by_id:deleted_by_id_int + } ); + } +} + +module.exports = Role; \ No newline at end of file diff --git a/src/models/user_roles.model.js b/src/models/user_roles.model.js new file mode 100644 index 0000000..07080bf --- /dev/null +++ b/src/models/user_roles.model.js @@ -0,0 +1,121 @@ +/** + * @file User roles model for phase.user_roles table + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} UserRole + * @property {number} user_id - User ID (foreign key) + * @property {number} role_id - Role ID (foreign key) + */ + +/** + * User role model class + * @extends Model + */ +class UserRole extends Model { + /** + * Create a UserRole instance + * @param {Partial} [props] - User role properties + */ + constructor( props ) { + super( props ); + this.table = 'phase.user_roles'; + this.prepend = 'ur'; + this.default_columns = ['user_id', 'role_id']; + this.update_exclude_columns = ['user_id', 'role_id']; + this.base_query = ` + SELECT ur.user_id, ur.role_id + FROM phase.user_roles ur + `; + this.base_list_query = this.base_query; + this.default_order_by = 'ORDER BY ur.user_id ASC'; + this.instance = _props => new UserRole( _props ); + } + + /** + * Add a user-role relation + * @param {number|string} user_id - User ID + * @param {number|string} role_id - Role ID + * @returns {Promise} Created relation instance + * @throws {ValidationError} If IDs are invalid + * @throws {FailedToCreateError} If creation fails + */ + static async add_relation( user_id, role_id ) { + const user_id_int = parseInt( user_id, 10 ); + const role_id_int = parseInt( role_id, 10 ); + if (isNaN( user_id_int ) || isNaN( role_id_int )) { + throw new ValidationError( 'user_id and role_id must be valid integers' ); + } + const query_str = ` + INSERT INTO phase.user_roles (user_id, role_id) + VALUES ($1, $2) + RETURNING *; + `; + const values = [user_id_int, role_id_int]; + const result = await phsdb.query( query_str, values, { plain:true } ); + if (!result) throw new FailedToCreateError( 'Failed to add user-role relation' ); + return new UserRole( result ); + } + + /** + * Remove a user-role relation + * @param {number|string} role_id - Role ID + * @param {number|string|undefined} [user_id=undefined] - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {ValidationError} If IDs are invalid + */ + static async remove_relation( role_id, user_id = undefined ) { + const user_id_int = parseInt( user_id, 10 ); + const role_id_int = parseInt( role_id, 10 ); + if (isNaN( role_id_int )) { + throw new ValidationError( 'role_id must be a valid integer' ); + } + let query_str = ` + DELETE + FROM phase.user_roles + WHERE role_id = $1 ${ user_id_int ? 'AND user_id = $2' : '' } + RETURNING *; + `; + const values = [role_id_int]; + if (!isNaN( user_id_int )) values.push( user_id_int ); + const result = await phsdb.query( query_str, values, { plain:!isNaN( user_id_int ) } ); + return result ? new UserRole( result ) : null; + } + + /** + * Find user-role relation by IDs + * @param {number|string} user_id - User ID + * @param {number|string} role_id - Role ID + * @param {string[]} [excludes] - Fields to exclude from result + * @returns {Promise} Relation instance or null + */ + static async find_by_ids( user_id, role_id, excludes = [] ) { + return await new UserRole().find_one( + { user_id, role_id }, + excludes + ); + } + + /** + * Find user-role relations by user ID + * @param {number|string} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + */ + static async find_by_user_id( user_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new UserRole().find_many( + { user_id }, + excludes, + order_by, + limit, + offset + ); + } +} + +module.exports = UserRole; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 7069380..0633277 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,11 +6,13 @@ module.exports.APIRoutes = ( passport ) => { router.use( '/git', require( './git.routes' )( passport ) ); router.use( '/docker', require( './docker.routes' )( passport ) ); router.use( '/vpn', require( './vpn.routes' )( passport ) ); - router.use( '/users', require( './user.routes' )( passport ) ); + router.use( '/user', require( './user.routes' )( passport ) ); router.use( '/phone_number', require( './phone_number.routes' )( passport ) ); router.use( '/user_phone_numbers', require( './user_phone_numbers.routes' )( passport ) ); router.use( '/address', require( './address.routes' )( passport ) ); router.use( '/user_addresses', require( './user_addresses.routes' )( passport ) ); router.use( '/authentication', require( './authentication.routes' )( passport ) ); + router.use( '/role', require( './role.routes' )( passport ) ); + router.use( '/user_roles', require( './user_roles.routes' )( passport ) ); return router; }; diff --git a/src/routes/role.routes.js b/src/routes/role.routes.js new file mode 100644 index 0000000..2c6a2d1 --- /dev/null +++ b/src/routes/role.routes.js @@ -0,0 +1,23 @@ +/** + * @file Role routes configuration + */ + +const express = require( 'express' ); +const router = express.Router(); +const { validateAuth } = require( '../middleware/routeHelpers' ); +const role_controller = require( '../controllers/role.controller' ); + +/** + * Configure role routes + * @param {Object} passport - Passport instance for authentication + * @returns {Object} Express router with role routes + */ +module.exports = ( passport ) => { + router.post( '/create', validateAuth( passport ), role_controller.create ); + router.get( '/name/:name', validateAuth( passport ), role_controller.find_by_name ); + router.get( '/:id', validateAuth( passport ), role_controller.find_one ); + router.get( '/', validateAuth( passport ), role_controller.find_many ); + router.put( '/:id', validateAuth( passport ), role_controller.update ); + router.put( '/:id/soft_delete', validateAuth( passport ), role_controller.soft_delete ); + return router; +}; \ No newline at end of file diff --git a/src/routes/user_roles.routes.js b/src/routes/user_roles.routes.js new file mode 100644 index 0000000..f5661a2 --- /dev/null +++ b/src/routes/user_roles.routes.js @@ -0,0 +1,21 @@ +/** + * @file User roles routes configuration + */ + +const express = require( 'express' ); +const router = express.Router(); +const { validateAuth } = require( '../middleware/routeHelpers' ); +const user_roles_controller = require( '../controllers/user_roles.controller' ); + +/** + * Configure user roles routes + * @param {Object} passport - Passport instance for authentication + * @returns {Object} Express router with user roles routes + */ +module.exports = ( passport ) => { + router.post( '/add', validateAuth( passport ), user_roles_controller.add_relation ); + router.delete( '/remove', validateAuth( passport ), user_roles_controller.remove_relation ); + router.get( '/ids/:user_id/:role_id', validateAuth( passport ), user_roles_controller.find_by_ids ); + router.get( '/user/:user_id', validateAuth( passport ), user_roles_controller.find_by_user_id ); + return router; +}; \ No newline at end of file