--- /dev/null
+/**
+ * @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