]> PHS Git Server - phs-api.git/commitdiff
Cleaning up the authentication.model.js file.
authorcharleswrayjr <charleswrayjr@gmail.com>
Fri, 12 Sep 2025 02:34:34 +0000 (21:34 -0500)
committercharleswrayjr <charleswrayjr@gmail.com>
Fri, 12 Sep 2025 02:34:34 +0000 (21:34 -0500)
src/models/authentication.model.js [new file with mode: 0644]

diff --git a/src/models/authentication.model.js b/src/models/authentication.model.js
new file mode 100644 (file)
index 0000000..85c5dbc
--- /dev/null
@@ -0,0 +1,198 @@
+/**
+ * @file Authentication model for phase.authentication table
+ */
+
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} Authentication
+ * @property {number} id - Authentication ID (primary key)
+ * @property {number} user_id - User ID (foreign key)
+ * @property {string} password - Hashed password
+ * @property {string} password_salt - Password salt
+ * @property {string|null} password_verification_token - Verification token
+ * @property {Date|null} password_verification_token_expiry - Token expiry
+ * @property {Date|null} last_password_failure_date - Last failed login attempt
+ * @property {number} password_failures_since_last_success - Failure count
+ * @property {boolean} is_deleted - Soft delete flag
+ * @property {number|null} deleted_by_id - ID of user who deleted this record
+ * @property {Date|null} deleted_at - Deletion timestamp
+ * @property {string|null} password_reset_token - Reset token
+ * @property {Date|null} password_reset_expire_date - Reset token expiry
+ * @property {boolean|null} is_locked - Account lock status
+ * @property {Date|null} locked_date - Lock timestamp
+ */
+
+/**
+ * Authentication model class
+ * @extends Model
+ */
+class Authentication extends Model {
+  /**
+   * Create an Authentication instance
+   * @param {Partial<Authentication>} [props] - Authentication properties
+   */
+  constructor( props ) {
+    super( props );
+    this.table = 'phase.authentication';
+    this.prepend = 'auth';
+    this.default_columns = [
+      'id', 'user_id', 'password', 'password_salt', 'password_verification_token',
+      'password_verification_token_expiry', 'last_password_failure_date',
+      'password_failures_since_last_success', 'is_deleted', 'deleted_by_id',
+      'deleted_at', 'password_reset_token', 'password_reset_expire_date',
+      'is_locked', 'locked_date'
+    ];
+    this.update_exclude_columns = ['id', 'user_id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id'];
+    this.base_query = `
+        SELECT auth.id,
+               auth.user_id,
+               auth.password,
+               auth.password_salt,
+               auth.password_verification_token,
+               auth.password_verification_token_expiry,
+               auth.last_password_failure_date,
+               auth.password_failures_since_last_success,
+               auth.is_deleted,
+               auth.deleted_by_id,
+               auth.deleted_at,
+               auth.password_reset_token,
+               auth.password_reset_expire_date,
+               auth.is_locked,
+               auth.locked_date
+        FROM phase.authentication auth
+        WHERE auth.is_deleted = false
+    `;
+    this.base_list_query = `
+        SELECT auth.id,
+               auth.user_id,
+               auth.password_verification_token,
+               auth.password_verification_token_expiry,
+               auth.last_password_failure_date,
+               auth.password_failures_since_last_success,
+               auth.password_reset_token,
+               auth.password_reset_expire_date,
+               auth.is_locked,
+               auth.locked_date
+        FROM phase.authentication auth
+        WHERE auth.is_deleted = false
+    `;
+    this.default_order_by = 'ORDER BY auth.user_id ASC';
+    this.instance = _props => new Authentication( _props );
+  };
+
+  /**
+   * Create a new authentication record
+   * @param {Omit<Authentication, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} auth_data - Authentication data
+   * @returns {Promise<Authentication>} Created authentication instance
+   * @throws {ValidationError} If required fields are missing
+   * @throws {FailedToCreateError} If creation fails
+   */
+  static async create( auth_data ) {
+    const {
+      user_id, password, password_salt, password_verification_token = null,
+      password_verification_token_expiry = null, last_password_failure_date = null,
+      password_failures_since_last_success = 0, password_reset_token = null,
+      password_reset_expire_date = null, is_locked = false, locked_date = null
+    } = auth_data;
+    if (!user_id || !password || !password_salt) {
+      throw new ValidationError( 'Missing required fields: user_id, password, password_salt' );
+    }
+    const query_str = `
+        INSERT INTO phase.authentication (user_id, password, password_salt, password_verification_token,
+                                          password_verification_token_expiry, last_password_failure_date,
+                                          password_failures_since_last_success, password_reset_token,
+                                          password_reset_expire_date, is_locked, locked_date)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
+        RETURNING *;
+    `;
+    const values = [
+      user_id, password, password_salt, password_verification_token,
+      password_verification_token_expiry, last_password_failure_date,
+      password_failures_since_last_success, password_reset_token,
+      password_reset_expire_date, is_locked, locked_date
+    ];
+    const result = await phsdb.query( query_str, values, { plain:true } );
+    if (!result) throw new FailedToCreateError( 'Failed to create authentication record' );
+    return new Authentication( result );
+  };
+
+  /**
+   * Find authentication record by user ID
+   * @param {number|string} user_id - User ID
+   * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude from result
+   * @returns {Promise<Authentication|null>} Authentication instance or null
+   * @throws {NotFoundError} If record not found
+   * @throws {TypeError} If user_id is invalid
+   */
+  static async find_by_user_id( user_id, excludes = ['password', 'password_salt'] ) {
+    return await new Authentication().find_one( { user_id }, excludes );
+  };
+
+  /**
+   * Find authentication record by password reset token
+   * @param {string} password_reset_token - Password reset token
+   * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude from result
+   * @returns {Promise<Authentication|null>} Authentication instance or null
+   * @throws {NotFoundError} If record not found
+   * @throws {TypeError} If password_reset_token is invalid
+   */
+  static async find_by_reset_token( password_reset_token, excludes = ['password', 'password_salt'] ) {
+    return await new Authentication().find_one( { password_reset_token }, excludes );
+  };
+
+  /**
+   * Soft delete authentication record
+   * @param {number|string} deleted_by_id - ID of user performing deletion
+   * @returns {Promise<Authentication>} Updated authentication 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
+    } );
+  };
+
+  /**
+   * Lock user account
+   * @returns {Promise<Authentication>} Updated authentication instance
+   * @throws {NotFoundError} If record not found
+   */
+  async lock_account() {
+    return await this.update( {
+      is_locked:true,
+      locked_date:new Date().toISOString()
+    } );
+  };
+
+  /**
+   * Unlock user account
+   * @returns {Promise<Authentication>} Updated authentication instance
+   * @throws {NotFoundError} If record not found
+   */
+  async unlock_account() {
+    return await this.update( {
+      is_locked:false,
+      locked_date:null
+    } );
+  };
+
+  // noinspection JSUnusedGlobalSymbols
+  /**
+   * Get authentication data without sensitive information
+   * @returns {Omit<Authentication, 'password'|'password_salt'>} Authentication data
+   */
+  to_safe_json() {
+    const { password, password_salt, ...safe_data } = this.toJSON();
+    return safe_data;
+  };
+}
+
+module.exports = Authentication;
\ No newline at end of file