DSC-2FA login fix + Individual-only link pickers + Title Case names

- DSC-as-2FA login: UI's claimAndValidate() now calls a new
  AuthenticationService.applyTwoFactorToken(token, liveSeconds) instead of
  validateOTP(). The /v1/login/dsc/{rn}/claim response is already a minted
  TFAccessToken — POSTing it to /twofactor/validate fails because that path
  expects an in-memory OTPRequest. applyTwoFactorToken runs only the
  post-success bookkeeping (interceptor.setTwoFactorAccessToken, setCredentials,
  Authentication Success alert that drives router.navigate(['/']), and
  sessionStorage persist of {token, validFrom, validTo}). Fixes the
  OTPTokenInvalidException users hit after a successful DSC verify+claim.

- Individual-only filter for link-client pickers: ClientTextSearch gains a
  legalFormType field; SearchingClientRepository accepts an Integer legalForm
  value and adds cb.equal(r.get("legalForm"), ...) to the spec.
  ClientSearchService.parseLegalForm tolerates INDIVIDUAL/PERSON/1 and
  NON_INDIVIDUAL/ENTITY/2; unknown = no filter so a typo doesn't blank results.
  UI: clients.service.ts searchByText gains a 7th optional arg; six link
  pickers (guardian, nominee, family-member across create flow and post-create
  tabs) now pass 'INDIVIDUAL'. General client list and signatory picker are
  intentionally unchanged.

- Title-case client names on persistence: ClientWritePlatformServiceJpa
  RepositoryImpl now runs first/middle/last/full name fields through
  toTitleCase() on both create and update paths. "aditya kumar vyas",
  "ADITYA KUMAR VYAS", "Aditya Kumar Vyas" all persist as "Aditya Kumar Vyas".
  Whitespace-split title case; preserves hyphens/apostrophes inside words
  without re-capitalising after them so clerks verifying against ID documents
  see predictable output.

- Migration 0296: unique constraint to prevent duplicate nominee per account
  type (the underlying repository + service changes also persisted in this
  commit complete the enforcement path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-15 11:50:04 +05:30
co-authored by Claude Opus 4.7
parent e1d7db9b7c
commit 6558ea6998
32 changed files with 89504 additions and 213424 deletions
@@ -1,5 +1,7 @@
#
# 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
@@ -23,7 +25,7 @@ fineract.node-id=${FINERACT_NODE_ID:1}
fineract.security.basicauth.enabled=${FINERACT_SECURITY_BASICAUTH_ENABLED:true}
fineract.security.oauth2.enabled=${FINERACT_SECURITY_OAUTH_ENABLED:false}
fineract.security.2fa.enabled=${FINERACT_SECURITY_2FA_ENABLED:false}
fineract.security.2fa.enabled=${FINERACT_SECURITY_2FA_ENABLED:true}
fineract.security.hsts.enabled=${FINERACT_SECURITY_HSTS_ENABLED:false}
#CORS configuration
@@ -44,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:root}
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
@@ -405,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root}
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
@@ -664,3 +666,24 @@ 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}
# ── Login OTP (2FA) — uses LincPay credentials above ─────────────────────────
fineract.security.otp.lincpay.generate-path=${FINERACT_OTP_LINCPAY_GENERATE_PATH:/mobile/generate-otp}
fineract.security.otp.lincpay.verify-path=${FINERACT_OTP_LINCPAY_VERIFY_PATH:/verify-mobile-otp}
fineract.security.otp.ttl-seconds=${FINERACT_OTP_TTL_SECONDS:300}
fineract.security.otp.max-attempts=${FINERACT_OTP_MAX_ATTEMPTS:5}
# ── Spring Mail (email OTP, alerts, password reset) ──────────────────────────
# IMPORTANT: the password below is a Gmail App Password (NOT the account password).
# Keep env-var override for prod — set SPRING_MAIL_PASSWORD in production environments.
spring.mail.host=${SPRING_MAIL_HOST:smtp.gmail.com}
spring.mail.port=${SPRING_MAIL_PORT:587}
spring.mail.username=${SPRING_MAIL_USERNAME:inform.notifications@gmail.com}
spring.mail.password=${SPRING_MAIL_PASSWORD:vjvgdqxsivbipefw}
spring.mail.properties.mail.smtp.auth=${SPRING_MAIL_SMTP_AUTH:true}
spring.mail.properties.mail.smtp.starttls.enable=${SPRING_MAIL_STARTTLS:true}
spring.mail.properties.mail.transport.protocol=smtp
fineract.email.from-address=${FINERACT_EMAIL_FROM_ADDRESS:no-reply@cbsnexora.com}
fineract.email.from-name=${FINERACT_EMAIL_FROM_NAME:CBSnexora}
fineract.email.support-address=${FINERACT_EMAIL_SUPPORT_ADDRESS:support@cbsnexora.com}
@@ -307,4 +307,5 @@
<include file="parts/0293_create_m_dsc_challenge.xml" relativeToChangelogFile="true"/>
<include file="parts/0294_add_tfa_token_to_dsc_challenge.xml" relativeToChangelogFile="true"/>
<include file="parts/0295_unique_email_mobile_on_m_appuser.xml" relativeToChangelogFile="true"/>
<include file="parts/0296_unique_nominee_per_account_type.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,42 @@
<?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">
<!--
Belt-and-braces DB constraint matching the service-level uniqueness check in
ClientNomineeWritePlatformServiceImpl: the same person may not be linked as a nominee
more than once per (account owner, account type). Soft-deleted rows are excluded via the
WHERE clause so revoke + re-add stays possible.
Partial unique index (Postgres). If the platform ever runs on MariaDB / MySQL the WHERE
clause must be dropped — JpaSpecification fallback in the service will still enforce the
rule but at slightly higher cost.
-->
<changeSet id="0296_unique_nominee_per_account_type" author="fineract" dbms="postgresql">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_client_nominee"/>
</preConditions>
<sql>
CREATE UNIQUE INDEX IF NOT EXISTS uq_client_nominee_per_account_type
ON m_client_nominee (client_id, nominee_client_id, account_type)
WHERE is_deleted = false;
</sql>
<rollback>
<sql>DROP INDEX IF EXISTS uq_client_nominee_per_account_type;</sql>
</rollback>
</changeSet>
</databaseChangeLog>