Skip to content

USER ID / Login ID Update for Mobile Number Change - #146

Merged
vishwab1 merged 18 commits into
release-3.8.2from
feature/edit-employee-username
Sep 3, 2026
Merged

USER ID / Login ID Update for Mobile Number Change#146
vishwab1 merged 18 commits into
release-3.8.2from
feature/edit-employee-username

Conversation

@snehar-nd

Copy link
Copy Markdown
Contributor

📋 Description

JIRA ID:

AMM-2434

✅ Type of Change

  • New feature (non-breaking change which adds functionality)

ℹ️ Additional Information

Please describe how the changes were tested, and include any relevant screenshots, logs, or other information that provides additional context.

Sneha and others added 15 commits August 25, 2026 16:03
…ring Employee ID

Edit no longer silently nulls Employee ID on every save — the edit
form doesn't submit that field, and the code was unconditionally
copying the (missing) value over the existing one. Also checks the
new username isn't already taken by another employee before saving,
rather than allowing silent duplicates.
CreatedBy/ModifiedBy across AMRIT store the username as a
denormalised string rather than a UserID foreign key, so renaming a
user has to be propagated by hand. Adds POST /username/renameUsername
plus a /preview variant that reports the same per-table row counts
without committing.

The sweep covers m_user and 28 RMNCH/FLW tables across db_iemr and
db_identity, in one transaction so a partial rename cannot be left
behind. Column naming was verified against the AMRIT-DB migrations:
db_identity is uniformly CreatedBy/ModifiedBy while db_iemr is split
between created_by/updated_by and CreatedBy/ModifiedBy.

Each table takes two UPDATEs, one per audit column, rather than a
single combined statement. Setting both columns off a CreatedBy match
would overwrite ModifiedBy on rows a different user last touched,
destroying their attribution, and would miss rows this user only
modified.

Usernames are bound as parameters; only whitelist constants are
interpolated. m_user is updated first so a unique-key rejection on
UserName or EmployeeID happens before anything else is written, and
the new name is capped at 12 characters when contact numbers are
included because m_user.ContactNo is varchar(12).

Scope is deliberately these tables only — it is not every table
carrying an audit column, so the old username survives elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preview timed out behind nginx on UAT (504). Each of the 28 tables was
counted with `createdBy = :u OR modifiedBy = :u`, and an OR across two
columns cannot use a single-column index on either — so every table was
a full scan. Split into two single-column counts, matching the two
UPDATEs the rename already issues, so both can use the new indexes in
AMRIT-DB V96/V19.

Splitting also makes the numbers honest: a row the user both created
and last modified is updated by each statement, so counting it once
under-reported what rename() goes on to report. Preview now also counts
m_user, which it previously skipped while the rename reported it, so
the two line up row for row.

Corrects the ELIGIBLE_COUPLE_TRACKING entry to eligible_couple_tracking
— the schema creates it lowercase, and table names are case-sensitive
on Linux MySQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reworks the sweep to the supplied query shape: each UPDATE now selects
the rows to touch by primary key through a derived table rather than
filtering on the audit column directly. The SELECT ... AS temp wrapper
is what MySQL requires to read from the same table an UPDATE targets
(error 1093), and driving by PK keeps the statements usable under
safe-update mode.

Carries each table's primary key in the registry; all 28 were verified
against the AMRIT-DB migrations.

Both audit columns are now set off a CreatedBy match, per the supplied
queries, replacing the earlier split into one statement per column.
Preview counts CreatedBy alone to match, and still reports m_user, so
preview and result line up row for row.

Keeps eligible_couple_tracking lower case: the schema creates it that
way and MySQL table names are case sensitive on Linux.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes POST /username/renameUsername/preview along with the service
method behind it, the two counting queries it used, and the now-unused
preview flag on the response.

/username/renameUsername is unchanged and remains the only endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EmployeeID was hardcoded to the new username in both branches of the
m_user update. It is now only touched when the request supplies a new
value, so renaming a username no longer implies changing the Employee
ID. The SET clause is built from what was actually asked for.

Splits the uniqueness check in two. UserName and EmployeeID hold
separate UNIQUE keys on m_user, so each conflicts only with its own
column — the previous combined check rejected a new username merely
because some row used it as an Employee ID, which was only correct
while the two were forced to match.

Both checks exclude the row being renamed, so re-entering the value
that row already holds is not reported as a conflict with itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds POST /username/checkAvailability so the screen can report a
duplicate as it is typed rather than at submit. It reuses the same
repository checks rename() runs, so the two cannot disagree — including
counting soft-deleted rows, which still hold m_user's UNIQUE keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both new values are now optional. Each is normalised to null when blank
or already equal to what the row holds, and the m_user UPDATE is built
from whichever columns actually change — so a username-only change, an
employee-ID-only change, or both are all valid. The request is rejected
only when neither would change anything.

The 28-table audit sweep is skipped entirely when the username is
unchanged: CreatedBy/ModifiedBy record the username, so an
employee-ID-only change leaves every audit row already correct. That
also keeps such a change off the unindexed scan path.

Drops the updateEmployeeId flag (supplying a value is the intent) and
the /username/checkAvailability endpoint, since the screen now uses the
same m/FindEmployeeByName and m/FindEmployeeDetails checks as Employee
Master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes userID on the rename request and uses it as the identity of the
row being changed. The username is the very thing being renamed, so a
string match is a poor way to address it; the primary key is not.

The m_user UPDATE is now keyed on UserID, and both uniqueness checks
exclude self by UserID rather than by username — the same way the
Employee Master edit path does it via findEmployeeByNameForUpdate.

The old username is now read from the row rather than trusted from the
request, and a mismatch is rejected: if the row was renamed after the
screen loaded its list, sweeping on the stale username would repoint
the wrong audit records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The response carried only the username pair, so a caller had no way to
see what happened to the employee ID. Adds oldEmployeeId (read before
the update) and newEmployeeId.

Also adds userNameUpdated/employeeIdUpdated booleans. OutputResponse
re-serialises the envelope with a GsonBuilder that has no
serializeNulls, so any null field drops out of the JSON entirely —
which is why an unset employee ID appears to be missing rather than
null. A primitive boolean always survives that pass, so the caller can
tell "unchanged" from "absent".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes rowsPerTable, tablesAffected and totalRowsAffected. The
response now reports only what changed: the username and employee ID
before and after, plus the flags saying whether each was written.

The total row count is still summed and logged, so the scale of a
rename remains traceable without exposing the table list to callers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…top clearing Employee ID"

This reverts commit 4f6371f.
Reverts 4f6371f — the editUserDetails uniqueness check, the Employee ID
null-guard and findEmployeeByNameForUpdate. Verified independent of the
Change Username feature: nothing in the username package called any of
it, so the rename API is unaffected.

Drops the comment that pointed at the removed query.

Note this restores the prior editUserDetails behaviour, including the
unconditional setEmployeeID that nulls Employee ID when the edit form
does not submit that field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pure deletions — no code changed. GPL license headers kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 16cfe6d8-af29-499c-9b10-21762e11556f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

+ "WHERE %4$s IN (SELECT %4$s FROM (SELECT %4$s FROM %1$s WHERE %2$s = :oldUserName) AS temp)",
table.getQualifiedName(), table.getCreatedByColumn(), table.getModifiedByColumn(),
table.getPrimaryKeyColumn());
Query query = entityManager.createNativeQuery(sql);
}

Query query = entityManager.createNativeQuery(
"UPDATE db_iemr.m_user SET " + String.join(", ", assignments) + " WHERE UserID = :userID");
Sneha and others added 2 commits September 2, 2026 15:14
Log injection (UsernameRenameServiceImpl): the rename start line logged
the old/new username and employee ID straight from the request, so a
value containing CR/LF could forge log entries. Those values are now
flattened to a safe character set and length-capped before logging, and
the line also carries the userID, which is an integer and cannot be
tampered with.

Dynamic SQL (UsernameRenameRepository): schema, table and column names
cannot be bound as parameters, so they are interpolated. Each is now
checked against a strict SQL-identifier pattern before it can reach a
statement, so anything that is not a plain identifier throws instead of
being concatenated in. All caller-supplied data was, and remains, bound.
Verified every identifier in the table registry satisfies the pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sanitising helper did not clear Sonar's taint analysis — a custom
regex replace is not recognised as a sanitiser, so the rule still traced
request data into the logger and the Quality Gate stayed at Security
Rating C.

Logs the userID and two booleans saying which columns are changing
instead. Nothing request-derived reaches the log, so the taint path is
gone rather than filtered. The values themselves are still available in
the response and in m_user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +122 to +124
Query query = entityManager.createNativeQuery(String.format(
"SELECT COUNT(*) FROM db_iemr.m_user WHERE %s = :value AND UserID <> :excludeUserID",
identifier(column)));
Sonar reported three MAJOR vulnerabilities for dynamically formatted SQL
in UsernameRenameRepository, holding New Code at Security Rating C and
failing the Quality Gate.

Each query call now receives a constant rather than a formatted string:

- the two uniqueness counts become two fixed queries instead of one with
  an interpolated column name
- the m_user update selects one of five constants covering the possible
  combinations, instead of joining a SET clause at runtime
- the per-table rename SQL is built once in AuditTable's constructor and
  read back through a getter, so the query call takes a plain value

Identifier validation moves to AuditTable, where the identifiers now
live, and rejects anything that is not a plain SQL identifier.

The generated SQL is unchanged: verified byte-identical for the
per-table rename and for all eight combinations of the m_user update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@vishwab1
vishwab1 merged commit c6ada0a into release-3.8.2 Sep 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants