nhance/app/Controllers/HrLoginDiagnoseController.php
2026-08-01 11:56:03 +05:30

637 lines
29 KiB
PHP

<?php
namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
/**
* Standalone HR login diagnostic tool.
* Does not modify existing auth / HR access controllers.
*/
class HrLoginDiagnoseController extends AdminController
{
use ResponseTrait;
public function index()
{
$email = trim((string) $this->request->getGet('email'));
$mobile = trim((string) $this->request->getGet('mobile'));
$data = [
'title' => 'HR Login Diagnostic',
'email' => $email,
'mobile' => $mobile,
'result' => null,
'has_search' => false,
];
if ($email !== '' || $mobile !== '') {
$data['has_search'] = true;
$data['result'] = $this->diagnose($email, $mobile);
}
return view('hr_login_diagnose', $data);
}
public function check()
{
try {
$payload = $this->request->getJSON(true);
if (!is_array($payload) || empty($payload)) {
$payload = [
'email' => $this->request->getPost('email'),
'mobile' => $this->request->getPost('mobile'),
];
}
$email = trim((string) ($payload['email'] ?? ''));
$mobile = trim((string) ($payload['mobile'] ?? ''));
if ($email === '' && $mobile === '') {
return $this->respond([
'status' => false,
'message' => 'Enter email or mobile.',
], 400);
}
$result = $this->diagnose($email, $mobile);
return $this->respond([
'status' => true,
'data' => $result,
], 200);
} catch (\Throwable $e) {
log_message('error', 'HrLoginDiagnose check failed: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
return $this->respond([
'status' => false,
'message' => 'Diagnosis failed: ' . $e->getMessage(),
], 500);
}
}
private function diagnose(string $email, string $mobile): array
{
$issues = [];
$postDb = \Config\Database::connect();
$contacts = $this->fetchPostContacts($postDb, $email, $mobile);
if (empty($contacts)) {
$issues[] = $this->issue(
'HR_NOT_FOUND',
'high',
'No active HR found in post level_contacts for the given email/mobile (contact_type=client).',
[
'Option A: Verify email/mobile spelling and that level_contacts.contact_type = client and is_active = 1.',
'Option B: Create/activate the HR contact under the correct client_branch (ref_id = branch id).',
'Option C: If HR exists only in pre DB, create the matching post level_contacts row first.',
]
);
return [
'input' => ['email' => $email, 'mobile' => $mobile],
'overall_status' => 'FAIL',
'summary' => 'HR not found in post DB.',
'issues' => $issues,
'problems' => $issues,
'post' => ['contacts' => []],
'hr_access_control'=> [],
'pre' => ['reachable' => null, 'mappings' => []],
'login_simulation' => [
'would_authenticate' => false,
'cards' => [],
],
];
}
$accessRows = [];
$preMaps = [];
$loginCards = [];
$pairRows = [];
$preReachable = null;
try {
$preDb = \Config\Database::connect('preDB');
$preDb->connect();
$preReachable = true;
} catch (\Throwable $e) {
$preReachable = false;
$preDb = null;
$issues[] = $this->issue(
'PRE_DB_UNREACHABLE',
'high',
'Could not connect to preDB: ' . $e->getMessage(),
[
'Option A: Check preDB hostname/database/username/password in app/Config/Database.php or .env.',
'Option B: Confirm pre DB server is reachable from this host (network/firewall).',
'Option C: Re-run diagnose after connectivity is restored to validate PRE mapping.',
]
);
}
foreach ($contacts as $contact) {
$flags = [
'HR_FOUND' => true,
'BRANCH_ACTIVE' => (int) ($contact['branch_active'] ?? 0) === 1,
'CLIENT_ACTIVE' => (int) ($contact['client_active'] ?? 0) === 1,
];
if (!$flags['BRANCH_ACTIVE']) {
$issues[] = $this->issue(
'BRANCH_INACTIVE',
'high',
"Branch inactive for post_hr_id={$contact['post_hr_id']} (branch #{$contact['post_branch_id']}, {$contact['branch_name']}).",
[
"Option A: UPDATE client_branch SET is_active=1 WHERE id={$contact['post_branch_id']};",
'Option B: In Client Onboarding, activate this branch and re-check login.',
'Note: Login join requires client_branch.is_active=1.',
]
);
}
if (!$flags['CLIENT_ACTIVE']) {
$issues[] = $this->issue(
'CLIENT_INACTIVE',
'high',
"Client inactive for post_hr_id={$contact['post_hr_id']} (client #{$contact['post_client_id']}, {$contact['client_name']}).",
[
"Option A: UPDATE clients SET is_active=1 WHERE id={$contact['post_client_id']};",
'Option B: Activate the client in admin and re-run diagnose.',
'Note: Login join requires clients.is_active=1.',
]
);
}
$access = $this->fetchAccessRow(
$postDb,
(int) $contact['post_hr_id'],
(int) $contact['post_branch_id'],
(int) $contact['post_client_id']
);
$modules = [];
$hasModules = false;
$tokenReady = false;
$failureReason = null;
if (empty($access)) {
$failureReason = 'NO_HR_ACCESS_CONTROL_ROW';
$issues[] = $this->issue(
'NO_HR_ACCESS_CONTROL_ROW',
'high',
"No hr_access_control row for post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']} ({$contact['client_name']} / {$contact['branch_name']}).",
[
'Option A: Open Client Onboarding → HR Access for this client, select at least one module for this HR/branch, then Save.',
"Option B: Insert hr_access_control with post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']} and allowed_modules JSON.",
'Note: Without this row, getVerifiedHrData returns empty token for this card.',
]
);
} else {
$modules = json_decode($access['allowed_modules'] ?? '[]', true);
if (!is_array($modules)) {
$modules = [];
}
$hasModules = $this->hasAnyModule($modules);
$tokenReady = $hasModules;
if (!$hasModules) {
$failureReason = 'EMPTY_ALLOWED_MODULES';
$issues[] = $this->issue(
'EMPTY_ALLOWED_MODULES',
'high',
"hr_access_control id={$access['id']} exists but allowed_modules is empty for post_hr_id={$contact['post_hr_id']}.",
[
'Option A: Open HR Access Control, tick PRE/POST modules for this HR, Save.',
"Option B: UPDATE hr_access_control SET allowed_modules='{\"pre\":[1],\"post\":[2,3,4]}' WHERE id={$access['id']}; (adjust module ids as needed).",
'Note: Empty modules → token is treated as not ready.',
]
);
}
}
$accessRows[] = [
'id' => $access['id'] ?? null,
'post_hr_id' => (int) $contact['post_hr_id'],
'pre_hr_id' => isset($access['pre_hr_id']) ? (int) $access['pre_hr_id'] : null,
'post_branch_id' => (int) $contact['post_branch_id'],
'pre_branch_id' => isset($access['pre_branch_id']) ? (int) $access['pre_branch_id'] : null,
'post_client_id' => (int) $contact['post_client_id'],
'pre_client_id' => isset($access['pre_client_id']) ? (int) $access['pre_client_id'] : null,
'allowed_modules' => $modules,
'allowed_pre_policies' => json_decode($access['allowed_pre_policies'] ?? '[]', true) ?: [],
'allowed_active_policies' => json_decode($access['allowed_active_policies'] ?? '[]', true) ?: [],
'allowed_cd' => json_decode($access['allowed_cd'] ?? '[]', true) ?: [],
'is_active' => isset($access['is_active']) ? (int) $access['is_active'] : null,
'has_token' => $tokenReady,
'token_ready' => $tokenReady,
'failure_reason' => $failureReason,
'note' => $failureReason ?: 'OK',
];
$preMap = $this->buildPreMapping($preDb, $contact, $access);
$preMaps[] = $preMap;
if ($preReachable === true) {
if (empty($preMap['PRE_BRANCH_LINKED'])) {
$postBranchId = (int) ($contact['post_branch_id'] ?? 0);
$issues[] = $this->issue(
'PRE_BRANCH_NOT_LINKED',
'medium',
"Post branch #{$postBranchId} ({$contact['branch_name']}) has no valid pre_branch_id link (current pre_branch_id=" . ($contact['pre_branch_id'] ?? 'null') . ").",
[
"Option A: Set client_branch.pre_branch_id on post branch #{$postBranchId} to the matching pre client_branch.id.",
'Option B: Use Testing/mapping tools to sync post↔pre branch ids, then re-run diagnose.',
'Option C: Confirm the pre branch exists and is_active=1 before linking.',
'Note: Without this link, PRE HR/client mapping cannot be resolved.',
]
);
} elseif (empty($preMap['PRE_HR_MATCHED'])) {
$issues[] = $this->issue(
'PRE_HR_NOT_MATCHED',
'medium',
"No pre level_contacts match for email/mobile under pre client #{$preMap['pre_client_id']} "
. "(post_hr_id={$contact['post_hr_id']}, email={$contact['hr_mail']}, mobile={$contact['hr_mobile']}).",
[
'Option A: Align email AND mobile between pre and post level_contacts for this HR.',
"Option B: Create/activate pre HR contact under pre client #{$preMap['pre_client_id']} with the same email/mobile.",
'Option C: Check pre contact_type=client and is_active=1 on the matching branch.',
]
);
} elseif (empty($preMap['PRE_IDS_CONSISTENT']) && !empty($access)) {
$storedHr = (int) ($access['pre_hr_id'] ?? 0);
$storedClient = (int) ($access['pre_client_id'] ?? 0);
$storedBranch = (int) ($access['pre_branch_id'] ?? 0);
$liveHr = (int) ($preMap['pre_hr_id'] ?? 0);
$liveClient = (int) ($preMap['pre_client_id'] ?? 0);
$liveBranch = (int) ($preMap['pre_branch_id'] ?? 0);
$accessId = (int) ($access['id'] ?? 0);
$issues[] = $this->issue(
'PRE_IDS_INCONSISTENT',
'medium',
"hr_access_control pre ids do not match live pre data for post_hr_id={$contact['post_hr_id']} "
. "(client #{$contact['post_client_id']}, branch #{$contact['post_branch_id']}). "
. "Stored (hr_access_control id={$accessId}): pre_hr_id={$storedHr}, pre_client_id={$storedClient}, pre_branch_id={$storedBranch}. "
. "Live pre: pre_hr_id={$liveHr}, pre_client_id={$liveClient}, pre_branch_id={$liveBranch}.",
[
'Option A: Open Client Onboarding → HR Access for this client, select modules for this HR, Save (form must send live pre ids).',
"Option B (recommended): UPDATE hr_access_control SET pre_hr_id={$liveHr}, pre_client_id={$liveClient}, pre_branch_id={$liveBranch} WHERE id={$accessId};",
'Option C: If UI re-save keeps old ids, auto-refresh in saveHrAccessData is commented out — enable it or use Option B.',
'Note: POST login can still work; this mainly affects PRE modules/JWT pre mapping.',
]
);
}
}
$canLoginCard = $flags['BRANCH_ACTIVE'] && $flags['CLIENT_ACTIVE'] && $tokenReady;
$loginCards[] = [
'client' => $contact['client_name'] ?? '',
'branch' => $contact['branch_name'] ?? '',
'post_hr_id' => (int) $contact['post_hr_id'],
'post_client_id' => (int) $contact['post_client_id'],
'post_branch_id' => (int) $contact['post_branch_id'],
'has_token' => $canLoginCard,
'failure_reason' => $canLoginCard
? null
: ($failureReason
?: (!$flags['BRANCH_ACTIVE'] ? 'BRANCH_INACTIVE'
: (!$flags['CLIENT_ACTIVE'] ? 'CLIENT_INACTIVE' : 'UNKNOWN'))),
];
$pairRows[] = [
'post' => [
'post_hr_id' => (int) $contact['post_hr_id'],
'hr_name' => $contact['hr_name'] ?? '',
'hr_mail' => $contact['hr_mail'] ?? '',
'hr_mobile' => $contact['hr_mobile'] ?? '',
'is_active' => (int) ($contact['is_active'] ?? 0),
'post_branch_id' => (int) ($contact['post_branch_id'] ?? 0),
'branch_name' => $contact['branch_name'] ?? '',
'branch_active' => (int) ($contact['branch_active'] ?? 0),
'pre_branch_id' => $contact['pre_branch_id'] ?? null,
'post_client_id' => (int) ($contact['post_client_id'] ?? 0),
'client_name' => $contact['client_name'] ?? '',
'client_active' => (int) ($contact['client_active'] ?? 0),
'has_access' => ! empty($access),
'token_ready' => $tokenReady,
'hr_active' => (int) ($contact['is_active'] ?? 0) === 1,
],
'pre' => $preMap,
'access_id' => isset($access['id']) ? (int) $access['id'] : null,
'has_token' => $canLoginCard,
];
$contact['hr_found'] = true;
$contact['flags'] = $flags;
}
$anyToken = false;
foreach ($loginCards as $card) {
if (!empty($card['has_token'])) {
$anyToken = true;
break;
}
}
$wouldAuth = !empty($contacts);
$overall = ($wouldAuth && $anyToken && empty(array_filter($issues, static fn ($i) => ($i['severity'] ?? '') === 'high')))
? 'PASS'
: (($anyToken && $wouldAuth) ? 'WARN' : 'FAIL');
if ($overall === 'PASS') {
$summary = 'HR identity, access row, and login readiness look OK.';
$verdict = 'OK';
} elseif ($overall === 'WARN') {
$summary = 'Login may work for some cards, but mapping/access issues remain.';
$verdict = 'OK_WITH_WARNINGS';
} else {
$summary = 'HR login will fail or return empty token/modules.';
$verdict = 'WILL_FAIL';
}
// Deduplicate issues by code+message
$unique = [];
$deduped = [];
foreach ($issues as $issue) {
$key = ($issue['code'] ?? '') . '|' . ($issue['message'] ?? '');
if (isset($unique[$key])) {
continue;
}
$unique[$key] = true;
$deduped[] = $issue;
}
$postContacts = array_map(static function ($c) {
return [
'post_hr_id' => (int) $c['post_hr_id'],
'hr_name' => $c['hr_name'] ?? '',
'hr_mail' => $c['hr_mail'] ?? '',
'hr_mobile' => $c['hr_mobile'] ?? '',
'is_active' => (int) ($c['is_active'] ?? 0),
'post_branch_id' => (int) ($c['post_branch_id'] ?? 0),
'branch_name' => $c['branch_name'] ?? '',
'branch_active' => (int) ($c['branch_active'] ?? 0),
'pre_branch_id' => $c['pre_branch_id'] ?? null,
'post_client_id' => (int) ($c['post_client_id'] ?? 0),
'client_name' => $c['client_name'] ?? '',
'client_active' => (int) ($c['client_active'] ?? 0),
'hr_found' => true,
];
}, $contacts);
$firstPost = $postContacts[0] ?? [];
$firstPre = $preMaps[0] ?? [];
$tokenCards = array_filter($loginCards, static fn ($c) => ! empty($c['has_token']));
$preMatched = array_filter($preMaps, static fn ($m) => ! empty($m['PRE_HR_MATCHED']));
// Group PRE/POST pairs by post client for client-wise display.
$byClientMap = [];
foreach ($pairRows as $pair) {
$cid = (int) ($pair['post']['post_client_id'] ?? 0);
$key = (string) $cid;
if (! isset($byClientMap[$key])) {
$byClientMap[$key] = [
'post_client_id' => $cid,
'client_name' => $pair['post']['client_name'] ?? '',
'client_active' => (int) ($pair['post']['client_active'] ?? 0),
'rows' => [],
];
}
$byClientMap[$key]['rows'][] = $pair;
}
$byClient = array_values($byClientMap);
usort($byClient, static function ($a, $b) {
return strcasecmp((string) ($a['client_name'] ?? ''), (string) ($b['client_name'] ?? ''));
});
return [
'input' => ['email' => $email, 'mobile' => $mobile],
'overall_status' => $overall,
'verdict' => $verdict,
'verdict_message' => $summary,
'summary' => $summary,
'issues' => $deduped,
'problems' => $deduped,
'by_client' => $byClient,
'post' => [
'contacts' => $postContacts,
'raw_found' => count($postContacts) > 0,
'eligible' => $anyToken,
'filters_summary' => [
'hr_active' => ! empty($firstPost) && (int) ($firstPost['is_active'] ?? 0) === 1,
'branch_active' => ! empty($firstPost) && (int) ($firstPost['branch_active'] ?? 0) === 1,
'client_active' => ! empty($firstPost) && (int) ($firstPost['client_active'] ?? 0) === 1,
'has_access' => count($tokenCards) > 0,
'token_ready' => $anyToken,
],
'primary' => $firstPost,
],
'hr_access_control' => $accessRows,
'pre' => [
'reachable' => $preReachable,
'mappings' => $preMaps,
'raw_found' => count($preMatched) > 0,
'eligible' => count($preMatched) > 0,
'filters_summary' => [
'db_reachable' => $preReachable === true,
'PRE_BRANCH_LINKED' => ! empty($firstPre['PRE_BRANCH_LINKED']),
'PRE_HR_MATCHED' => ! empty($firstPre['PRE_HR_MATCHED']),
'PRE_IDS_CONSISTENT' => ! empty($firstPre['PRE_IDS_CONSISTENT']),
'PRE_CLIENT_MAPPED' => ! empty($firstPre['PRE_CLIENT_MAPPED']),
],
'primary' => $firstPre,
'error' => $preReachable === false ? 'preDB connection failed' : null,
],
'login_simulation' => [
'would_authenticate' => $wouldAuth,
'cards' => $loginCards,
],
'merge' => [
'post_eligible' => $anyToken,
'pre_eligible' => count($preMatched) > 0,
'post_contacts' => count($postContacts),
'pre_matched' => count($preMatched),
'token_cards' => count($tokenCards),
'access_rows' => count(array_filter($accessRows, static fn ($a) => ! empty($a['id']))),
'client_count' => count($byClient),
'would_return_both' => $anyToken && count($preMatched) > 0,
'winner' => ($anyToken && count($preMatched) > 0) ? 'both' : ($anyToken ? 'post' : (count($preMatched) > 0 ? 'pre' : 'none')),
'reason' => $summary,
],
];
}
private function fetchPostContacts($db, string $email, string $mobile): array
{
$builder = $db->table('level_contacts lc')
->select("
lc.id AS post_hr_id,
lc.name AS hr_name,
lc.email AS hr_mail,
lc.mobile AS hr_mobile,
lc.is_active,
cb.id AS post_branch_id,
cb.branch_name,
cb.is_active AS branch_active,
cb.pre_branch_id,
c.id AS post_client_id,
c.client_name,
c.is_active AS client_active
")
->join('client_branch cb', 'cb.id = lc.ref_id', 'left')
->join('clients c', 'c.id = cb.client_id', 'left')
->where('lc.contact_type', 'client')
->where('lc.is_active', 1);
if ($email !== '' && $mobile !== '') {
$builder->groupStart()
->where('lc.email', $email)
->orWhere('lc.mobile', $mobile)
->groupEnd();
} elseif ($email !== '') {
$builder->where('lc.email', $email);
} else {
$builder->where('lc.mobile', $mobile);
}
return $builder->get()->getResultArray();
}
private function fetchAccessRow($db, int $postHrId, int $postBranchId, int $postClientId): ?array
{
$row = $db->table('hr_access_control')
->where('post_hr_id', $postHrId)
->where('post_branch_id', $postBranchId)
->where('post_client_id', $postClientId)
->where('is_active', 1)
->get()
->getRowArray();
return $row ?: null;
}
private function hasAnyModule(array $modules): bool
{
if (isset($modules['pre']) || isset($modules['post'])) {
$pre = is_array($modules['pre'] ?? null) ? $modules['pre'] : [];
$post = is_array($modules['post'] ?? null) ? $modules['post'] : [];
return count($pre) > 0 || count($post) > 0;
}
return count($modules) > 0;
}
private function buildPreMapping($preDb, array $contact, ?array $access): array
{
$result = [
'post_hr_id' => (int) $contact['post_hr_id'],
'pre_hr_id' => null,
'pre_client_id' => null,
'pre_branch_id' => $contact['pre_branch_id'] ?? null,
'pre_client_name' => null,
'pre_branch_name' => null,
'hr_name' => null,
'hr_mail' => null,
'hr_mobile' => null,
'PRE_BRANCH_LINKED' => false,
'PRE_HR_MATCHED' => false,
'PRE_IDS_CONSISTENT' => false,
'PRE_CLIENT_MAPPED' => false,
'access_pre_hr_id' => isset($access['pre_hr_id']) ? (int) $access['pre_hr_id'] : null,
'access_pre_client_id' => isset($access['pre_client_id']) ? (int) $access['pre_client_id'] : null,
'access_pre_branch_id' => isset($access['pre_branch_id']) ? (int) $access['pre_branch_id'] : null,
];
if ($preDb === null) {
return $result;
}
$preBranchId = $contact['pre_branch_id'] ?? null;
if (empty($preBranchId)) {
return $result;
}
$preBranch = $preDb->table('client_branch cb')
->select('cb.id AS pre_branch_id, cb.client_id AS pre_client_id, cb.branch_name, c.client_name')
->join('clients c', 'c.id = cb.client_id', 'left')
->where('cb.id', $preBranchId)
->where('cb.is_active', 1)
->where('c.is_active', 1)
->get()
->getRowArray();
if (empty($preBranch)) {
return $result;
}
$result['PRE_BRANCH_LINKED'] = true;
$result['pre_branch_id'] = (int) $preBranch['pre_branch_id'];
$result['pre_client_id'] = (int) $preBranch['pre_client_id'];
$result['pre_branch_name'] = $preBranch['branch_name'] ?? null;
$result['pre_client_name'] = $preBranch['client_name'] ?? null;
$result['PRE_CLIENT_MAPPED'] = true;
$email = trim((string) ($contact['hr_mail'] ?? ''));
$mobile = trim((string) ($contact['hr_mobile'] ?? ''));
$preHrBuilder = $preDb->table('level_contacts lc')
->select('lc.id AS pre_hr_id, lc.name, lc.email, lc.mobile')
->join('client_branch cb', 'cb.id = lc.ref_id')
->where('lc.contact_type', 'client')
->where('lc.is_active', 1)
->where('cb.is_active', 1)
->where('cb.client_id', $preBranch['pre_client_id']);
if ($email !== '' && $mobile !== '') {
$preHrBuilder->where('lc.email', $email)->where('lc.mobile', $mobile);
} elseif ($email !== '') {
$preHrBuilder->where('lc.email', $email);
} elseif ($mobile !== '') {
$preHrBuilder->where('lc.mobile', $mobile);
}
$preHr = $preHrBuilder->get()->getRowArray();
if (!empty($preHr)) {
$result['PRE_HR_MATCHED'] = true;
$result['pre_hr_id'] = (int) $preHr['pre_hr_id'];
$result['hr_name'] = $preHr['name'] ?? null;
$result['hr_mail'] = $preHr['email'] ?? null;
$result['hr_mobile'] = $preHr['mobile'] ?? null;
}
if (!empty($access) && $result['PRE_HR_MATCHED']) {
$idsMatch =
(int) ($access['pre_hr_id'] ?? 0) === (int) $result['pre_hr_id']
&& (int) ($access['pre_client_id'] ?? 0) === (int) $result['pre_client_id']
&& (int) ($access['pre_branch_id'] ?? 0) === (int) $result['pre_branch_id'];
$result['PRE_IDS_CONSISTENT'] = $idsMatch;
} elseif (empty($access)) {
$result['PRE_IDS_CONSISTENT'] = false;
}
return $result;
}
/**
* @param string|array<int, string> $fix
* @return array{code:string,severity:string,message:string,fix:string,fixes:array<int,string>}
*/
private function issue(string $code, string $severity, string $message, $fix = ''): array
{
if (is_array($fix)) {
$fixes = array_values(array_filter(array_map('strval', $fix)));
} else {
$fixes = preg_split("/\r\n|\n|\r/", (string) $fix) ?: [];
$fixes = array_values(array_filter(array_map('trim', $fixes)));
}
return [
'code' => $code,
'severity' => $severity,
'message' => $message,
'fix' => implode("\n", $fixes),
'fixes' => $fixes,
];
}
}