merge feature/update_code and feature/Backend-MFA-Product branch code

This commit is contained in:
Nitesh Yadav
2026-05-11 10:44:57 +05:30
98 changed files with 56127 additions and 120067 deletions
@@ -664,3 +664,11 @@ fineract.openproject.api-token=${FINERACT_OPENPROJECT_API_TOKEN:57dd9f933f113c72
# ── DIP / MNRL Mobile Number Lookup ──────────────────────────────────────────
fineract.mobile-lookup.base-url=${FINERACT_MOBILE_LOOKUP_BASE_URL:http://localhost:8080}
# ── Email Configuration ──────────────────────────────────────────────────────
spring.mail.host=${FINERACT_EMAIL_SMTP_HOST:smtp.gmail.com}
spring.mail.port=${FINERACT_EMAIL_SMTP_PORT:587}
spring.mail.username=${FINERACT_EMAIL_SMTP_USERNAME:}
spring.mail.password=${FINERACT_EMAIL_SMTP_PASSWORD:}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.debug=false
@@ -294,4 +294,18 @@
<include file="parts/0280_add_fixed_deposit_product_codes.xml" relativeToChangelogFile="true"/>
<include file="parts/0284_remove_product_code_unique_constraints.xml" relativeToChangelogFile="true"/>
<include file="parts/0281_update_fd_base_interest_rate_seed_data.xml" relativeToChangelogFile="true"/>
<include file="parts/0278_add_soft_delete_to_family_members.xml" relativeToChangelogFile="true"/>
<include file="parts/0279_add_soft_delete_columns_client_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0280_add_deletion_audit_and_version_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0281_normalize_audit_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0282_create_m_otp_log.xml" relativeToChangelogFile="true"/>
<include file="parts/0283_add_mobile_to_m_appuser.xml" relativeToChangelogFile="true"/>
<include file="parts/0284_otp_log_support_email.xml" relativeToChangelogFile="true"/>
<include file="parts/0285_create_m_user_mfa_factor.xml" relativeToChangelogFile="true"/>
<include file="parts/0286_drop_obsolete_mobile_number_from_m_appuser.xml" relativeToChangelogFile="true"/>
<include file="parts/0287_mfa_factor_enforcement_config.xml" relativeToChangelogFile="true"/>
<include file="parts/0288_add_disabled_by_to_m_user_mfa_factor.xml" relativeToChangelogFile="true"/>
<include file="parts/0289_seed_read_otp_log_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0290_create_otp_log_auditor_role.xml" relativeToChangelogFile="true"/>
<include file="parts/0291_grant_read_otp_log_to_bankadmin_roles.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Soft-delete flags for family members. is_active=true / is_deleted=false by default. -->
<changeSet id="0278_add_is_active_is_deleted_to_family_members" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_family_members" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_family_members">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0278_index_family_members_active" author="fineract">
<preConditions onFail="MARK_RAN">
<not><indexExists tableName="m_family_members" indexName="idx_m_family_members_client_active"/></not>
</preConditions>
<createIndex tableName="m_family_members" indexName="idx_m_family_members_client_active">
<column name="client_id"/>
<column name="is_deleted"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,237 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Standardise soft-delete flags across all user-editable client detail tables.
Default state for a row: is_active=TRUE, is_deleted=FALSE.
On removal/unlink: is_active=FALSE, is_deleted=TRUE.
Read services must filter `WHERE is_deleted = false`.
Tables that already have `is_active` only get `is_deleted`.
Tables that have neither get both.
Skipped intentionally:
- m_client (uses status_enum / sub_status state machine)
- m_client_transaction (financial; reverse, don't delete)
- m_client_kyc_verification_log, m_client_transfer_details, m_client_attendance (immutable audit logs)
- m_ckyc_batch (uses its own `status` field)
-->
<!-- 1. Tables that already have is_active — add is_deleted only -->
<changeSet id="0279a_is_deleted_client_address" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_address" columnName="is_deleted"/></not>
</preConditions>
<addColumn tableName="m_client_address">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279b_is_deleted_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_nominee" columnName="is_deleted"/></not>
</preConditions>
<addColumn tableName="m_client_nominee">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279c_is_deleted_client_charge" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_charge" columnName="is_deleted"/></not>
</preConditions>
<addColumn tableName="m_client_charge">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- 2. Tables that have neither — add both -->
<changeSet id="0279d_flags_client_non_person" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_non_person" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_non_person">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279e_flags_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_identifier" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_identifier">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279f_flags_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_kyc" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_kyc">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279g_flags_address" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_address" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_address">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279h_flags_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_priority_sector" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_priority_sector">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279i_flags_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_non_priority_sector" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_non_priority_sector">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279j_flags_client_msme" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_msme" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_msme">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279k_flags_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_mudra_borrower" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_mudra_borrower">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279l_flags_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_startup_india" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_startup_india">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279m_flags_client_eou" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_eou" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_eou">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279n_flags_client_charge_paid_by" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_charge_paid_by" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_charge_paid_by">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0279o_flags_client_collateral_management" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_collateral_management" columnName="is_active"/></not>
</preConditions>
<addColumn tableName="m_client_collateral_management">
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Adds deletion-audit columns and an optimistic-locking version column to the
16 client-related tables already covered by 0278/0279 soft-delete columns.
- deleted_by BIGINT — FK-style m_appuser.id (matching existing created_by/last_modified_by style)
- deleted_at TIMESTAMP — when the soft-delete happened
- version BIGINT NOT NULL DEFAULT 0 — used by JPA @Version for optimistic locking
Maker-checker is intentionally NOT modeled per-row. Use the existing
m_portfolio_command_source framework + m_permission.can_maker_checker.
-->
<changeSet id="0280a_deletion_audit_family_members" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_family_members" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_family_members">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280b_deletion_audit_client_address" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_address" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_address">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280c_deletion_audit_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_nominee" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_nominee">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280d_deletion_audit_client_charge" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_charge">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280e_deletion_audit_client_non_person" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_non_person" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_non_person">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280f_deletion_audit_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_identifier" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_identifier">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280g_deletion_audit_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_kyc" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_kyc">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280h_deletion_audit_address" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_address" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_address">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280i_deletion_audit_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_priority_sector" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_priority_sector">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280j_deletion_audit_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_non_priority_sector" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_non_priority_sector">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280k_deletion_audit_client_msme" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_msme" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_msme">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280l_deletion_audit_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_mudra_borrower" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_mudra_borrower">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280m_deletion_audit_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_startup_india" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_startup_india">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280n_deletion_audit_client_eou" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_eou" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_eou">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280o_deletion_audit_client_charge_paid_by" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge_paid_by" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_charge_paid_by">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
<changeSet id="0280p_deletion_audit_client_collateral_management" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_collateral_management" columnName="deleted_at"/></not></preConditions>
<addColumn tableName="m_client_collateral_management">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
<column name="version" type="bigint" defaultValueNumeric="0"><constraints nullable="false"/></column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Normalises audit column names across the 16 client-related tables to:
created_by BIGINT (FK-style → m_appuser.id; matches existing platform pattern)
created_at TIMESTAMP
modified_by BIGINT (FK-style)
modified_on TIMESTAMP
Group A — already had columns under different names (renamed):
last_modified_by → modified_by
created_on_utc → created_at
last_modified_on_utc → modified_on
created_by → unchanged (already correctly named)
Group B — entities extend AbstractPersistableCustom and had no audit columns;
these are added fresh and populated via Spring Data JPA auditing.
Note: types are BIGINT (user IDs) to stay consistent with the rest of the
platform's existing audit. JOIN m_appuser to fetch usernames in reports.
-->
<!-- =========================== Group A: rename =========================== -->
<changeSet id="0281a_rename_audit_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_nominee" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_nominee" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_nominee" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_nominee" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281b_rename_audit_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_identifier" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_identifier" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_identifier" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_identifier" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281c_rename_audit_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_kyc" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_kyc" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_kyc" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_kyc" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281d_rename_audit_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_priority_sector" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_priority_sector" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_priority_sector" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_priority_sector" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281e_rename_audit_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_non_priority_sector" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281f_rename_audit_client_msme" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_msme" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_msme" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_msme" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_msme" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281g_rename_audit_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_mudra_borrower" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281h_rename_audit_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_startup_india" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_startup_india" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_startup_india" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_startup_india" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281i_rename_audit_client_eou" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_eou" columnName="last_modified_by"/></preConditions>
<renameColumn tableName="m_client_eou" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_eou" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_eou" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<!-- =========================== Group B: add ============================ -->
<changeSet id="0281j_add_audit_family_members" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_family_members" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_family_members">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
<changeSet id="0281k_add_audit_client_address" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_address" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_client_address">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
<!-- m_address intentionally skipped: it already has manual audit columns (created_by VARCHAR,
created_on DATE, updated_by VARCHAR, updated_on DATE) populated by service code. -->
<changeSet id="0281m_add_audit_client_charge" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_client_charge">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
<changeSet id="0281n_add_audit_client_non_person" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_non_person" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_client_non_person">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
<changeSet id="0281o_add_audit_client_charge_paid_by" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge_paid_by" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_client_charge_paid_by">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
<changeSet id="0281p_add_audit_client_collateral_management" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_collateral_management" columnName="created_at"/></not></preConditions>
<addColumn tableName="m_client_collateral_management">
<column name="created_by" type="bigint"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="bigint"/>
<column name="modified_on" type="datetime"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Audit log for OTP generation/verification. One row per OTP issued.
Used by login 2FA (LincPay provider) and extensible to other purposes.
-->
<changeSet id="0282_create_m_otp_log" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_otp_log"/></not></preConditions>
<createTable tableName="m_otp_log">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="purpose" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="provider" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="user_id" type="BIGINT"/>
<column name="username" type="VARCHAR(100)"/>
<column name="mobile_number" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="otp_reference" type="VARCHAR(100)"/>
<column name="otp_hash" type="VARCHAR(255)"/>
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="attempts" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="ip_address" type="VARCHAR(45)"/>
<column name="user_agent" type="VARCHAR(500)"/>
<column name="request_payload" type="LONGTEXT"/>
<column name="response_payload" type="LONGTEXT"/>
<column name="error_message" type="VARCHAR(1000)"/>
<column name="expires_at" type="datetime"/>
<column name="verified_at" type="datetime"/>
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="version" type="bigint" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_at" type="datetime"/>
<column name="modified_by" type="BIGINT"/>
<column name="modified_on" type="datetime"/>
</createTable>
<createIndex tableName="m_otp_log" indexName="idx_m_otp_log_user_status">
<column name="user_id"/>
<column name="status"/>
</createIndex>
<createIndex tableName="m_otp_log" indexName="idx_m_otp_log_otp_reference">
<column name="otp_reference"/>
</createIndex>
<createIndex tableName="m_otp_log" indexName="idx_m_otp_log_mobile_purpose">
<column name="mobile_number"/>
<column name="purpose"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Mobile number on m_appuser — used by login OTP (2FA) lookup. Nullable; populated at user create time. -->
<changeSet id="0283_add_mobile_no_to_m_appuser" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_appuser" columnName="mobile_no"/></not>
</preConditions>
<addColumn tableName="m_appuser">
<column name="mobile_no" type="VARCHAR(16)"/>
</addColumn>
<createIndex tableName="m_appuser" indexName="idx_m_appuser_mobile_no">
<column name="mobile_no"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Make m_otp_log work for both SMS-OTP and email-OTP. -->
<changeSet id="0284_otp_log_mobile_number_nullable" author="fineract">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_otp_log" columnName="mobile_number"/>
</preConditions>
<dropNotNullConstraint tableName="m_otp_log" columnName="mobile_number" columnDataType="VARCHAR(20)"/>
</changeSet>
<changeSet id="0284_otp_log_add_email_address" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_otp_log" columnName="email_address"/></not>
</preConditions>
<addColumn tableName="m_otp_log">
<column name="email_address" type="VARCHAR(255)"/>
</addColumn>
<createIndex tableName="m_otp_log" indexName="idx_m_otp_log_email_purpose">
<column name="email_address"/>
<column name="purpose"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Persists which MFA factors a given AppUser has enrolled and verified.
One row per (appuser_id, factor_type). Soft-disable via disabled_on.
-->
<changeSet id="0285_create_m_user_mfa_factor" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_user_mfa_factor"/></not></preConditions>
<createTable tableName="m_user_mfa_factor">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="appuser_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="factor_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="destination" type="VARCHAR(255)"/>
<column name="secret" type="VARCHAR(64)"/>
<column name="serial" type="VARCHAR(64)"/>
<column name="verified" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="verified_on" type="datetime"/>
<column name="otp_log_id" type="BIGINT"/>
<column name="created_on" type="datetime" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="disabled_on" type="datetime"/>
</createTable>
<addUniqueConstraint tableName="m_user_mfa_factor" columnNames="appuser_id, factor_type"
constraintName="uq_user_factor"/>
<addForeignKeyConstraint baseTableName="m_user_mfa_factor" baseColumnNames="appuser_id"
constraintName="fk_mfa_factor_user"
referencedTableName="m_appuser" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_user_mfa_factor" baseColumnNames="otp_log_id"
constraintName="fk_mfa_factor_otp"
referencedTableName="m_otp_log" referencedColumnNames="id"/>
<createIndex tableName="m_user_mfa_factor" indexName="idx_mfa_factor_user_active">
<column name="appuser_id"/>
<column name="disabled_on"/>
</createIndex>
</changeSet>
<!-- Permissions: list, register, disable factor (Self Service flow) -->
<changeSet id="0285_seed_user_mfa_permissions" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_USER_MFA_FACTORS'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="special"/>
<column name="code" value="READ_USER_MFA_FACTORS"/>
<column name="entity_name" value="USER_MFA_FACTOR"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="special"/>
<column name="code" value="CREATE_USER_MFA_FACTOR"/>
<column name="entity_name" value="USER_MFA_FACTOR"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="special"/>
<column name="code" value="DELETE_USER_MFA_FACTOR"/>
<column name="entity_name" value="USER_MFA_FACTOR"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Grant the new permissions to the Self Service User role (id resolved by name to keep portable). -->
<changeSet id="0285_grant_mfa_permissions_to_self_service_role" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Self Service User'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r, m_permission p
WHERE r.name = 'Self Service User'
AND p.code IN ('READ_USER_MFA_FACTORS', 'CREATE_USER_MFA_FACTOR', 'DELETE_USER_MFA_FACTOR')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
m_appuser ended up with both `mobile_number` (legacy from an earlier version of 0283) and
`mobile_no` (current). The Java code uses `mobile_no` exclusively. This migration:
1. Backfills `mobile_no` from `mobile_number` for any row where `mobile_no` is NULL
but `mobile_number` has a value, so we don't lose data.
2. Drops the now-unused `idx_m_appuser_mobile_number` index.
3. Drops the `mobile_number` column.
-->
<changeSet id="0286_backfill_mobile_no_from_mobile_number" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<columnExists tableName="m_appuser" columnName="mobile_number"/>
<columnExists tableName="m_appuser" columnName="mobile_no"/>
</and>
</preConditions>
<sql>
UPDATE m_appuser
SET mobile_no = mobile_number
WHERE mobile_no IS NULL
AND mobile_number IS NOT NULL
AND mobile_number &lt;&gt; ''
</sql>
</changeSet>
<changeSet id="0286_drop_index_idx_m_appuser_mobile_number" author="fineract">
<preConditions onFail="MARK_RAN">
<indexExists tableName="m_appuser" indexName="idx_m_appuser_mobile_number"/>
</preConditions>
<dropIndex tableName="m_appuser" indexName="idx_m_appuser_mobile_number"/>
</changeSet>
<changeSet id="0286_drop_mobile_number_from_m_appuser" author="fineract">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_appuser" columnName="mobile_number"/>
</preConditions>
<dropColumn tableName="m_appuser" columnName="mobile_number"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Global kill-switch for MFA-factor-backed login enforcement.
- DEFAULT: disabled. Existing users are unaffected on first deploy.
- When enabled, AuthenticationApiResource will challenge any user that
has at least one verified, non-disabled row in m_user_mfa_factor.
- Flip enabled=true in c_configuration to roll out; flip back to false
to roll back instantly without a redeploy.
-->
<changeSet id="0287_seed_mfa_factor_enforcement_config" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM c_configuration WHERE name = 'mfa-factor-enforcement'</sqlCheck>
</preConditions>
<insert tableName="c_configuration">
<column name="name" value="mfa-factor-enforcement"/>
<column name="enabled" valueBoolean="false"/>
<column name="is_trap_door" valueBoolean="false"/>
<column name="description"
value="When enabled, users with at least one verified MFA factor in m_user_mfa_factor are required to complete a 2FA challenge after password authentication."/>
</insert>
</changeSet>
<!--
Pre-flight: grant BYPASS_TWO_FACTOR to the Super user role so an admin
can always log in even if MFA delivery is broken (LincPay outage, SMTP
misconfig, etc.). Idempotent — only inserts if missing.
-->
<changeSet id="0287_grant_bypass_twofactor_to_super_user" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r, m_permission p
WHERE r.name = 'Super user'
AND p.code = 'BYPASS_TWO_FACTOR'
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Audit symmetry with our other soft-delete/disable patterns: capture WHO disabled the
factor, not just WHEN. Useful when admins disable a user's factor (future support) or
for compliance trails — paired with the existing disabled_on column.
-->
<changeSet id="0288_add_disabled_by_to_m_user_mfa_factor" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_user_mfa_factor" columnName="disabled_by"/></not>
</preConditions>
<addColumn tableName="m_user_mfa_factor">
<column name="disabled_by" type="BIGINT"/>
</addColumn>
<addForeignKeyConstraint baseTableName="m_user_mfa_factor" baseColumnNames="disabled_by"
constraintName="fk_mfa_factor_disabled_by"
referencedTableName="m_appuser" referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Admin-only read access to the m_otp_log audit trail. -->
<changeSet id="0289_seed_read_otp_log_permission" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_OTP_LOG'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="special"/>
<column name="code" value="READ_OTP_LOG"/>
<column name="entity_name" value="OTP_LOG"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Grant to Super user so admins can immediately use the new endpoint. -->
<changeSet id="0289_grant_read_otp_log_to_super_user" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r, m_permission p
WHERE r.name = 'Super user'
AND p.code = 'READ_OTP_LOG'
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Dedicated single-purpose role that grants READ access to the m_otp_log audit trail.
Sized small on purpose so admins can attach it to specific users (compliance auditors,
bank admins, support staff) without giving them the full Super user privileges.
The bankadmin assignment is environment-specific data — it's wrapped in a precondition so
the migration is a no-op on installs where that user doesn't exist.
-->
<!-- 1) Create the role -->
<changeSet id="0290_create_otp_log_auditor_role" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_role WHERE name = 'OTP Log Auditor'</sqlCheck>
</preConditions>
<insert tableName="m_role">
<column name="name" value="OTP Log Auditor"/>
<column name="description" value="Read-only access to the OTP audit trail (m_otp_log). Pair with users who need to inspect OTP send/verify history without having Super user rights."/>
<column name="is_disabled" valueBoolean="false"/>
</insert>
</changeSet>
<!-- 2) Grant READ_OTP_LOG to the role -->
<changeSet id="0290_grant_read_otp_log_to_otp_log_auditor" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'OTP Log Auditor'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_OTP_LOG'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r, m_permission p
WHERE r.name = 'OTP Log Auditor'
AND p.code = 'READ_OTP_LOG'
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
<!-- 3) Assign role to 'bankadmin' if that user exists in this environment.
This block is intentionally environment-aware — fresh installs without a bankadmin
user will skip silently (preCondition onFail=MARK_RAN). -->
<changeSet id="0290_assign_otp_log_auditor_to_bankadmin" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_appuser WHERE username = 'bankadmin'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'OTP Log Auditor'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_appuser_role (appuser_id, role_id)
SELECT u.id, r.id
FROM m_appuser u, m_role r
WHERE u.username = 'bankadmin'
AND r.name = 'OTP Log Auditor'
AND NOT EXISTS (
SELECT 1 FROM m_appuser_role ur
WHERE ur.appuser_id = u.id AND ur.role_id = r.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Direct grant: attach READ_OTP_LOG to every role the 'bankadmin' user already holds.
Migration 0290 created a dedicated 'OTP Log Auditor' role and linked it to bankadmin,
which is the clean approach. This migration is a parallel, belt-and-braces grant that
also adds the permission to bankadmin's *existing* roles (Bank Admin, etc.) so the
OTP Audit Logs screen works without anyone toggling checkboxes in Manage Roles UI.
Idempotent via NOT EXISTS — safe to re-run. No-op on installs without a 'bankadmin' user.
-->
<changeSet id="0291_grant_read_otp_log_to_bankadmin_roles" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_appuser WHERE username = 'bankadmin'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_OTP_LOG'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT ur.role_id, p.id
FROM m_appuser u
JOIN m_appuser_role ur ON ur.appuser_id = u.id
CROSS JOIN m_permission p
WHERE u.username = 'bankadmin'
AND p.code = 'READ_OTP_LOG'
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>