Show registration credentials in client business structure

This commit is contained in:
A R R R Associates
2026-09-02 17:45:18 +05:30
parent 1ce2fd070e
commit 0c5f555eb4
2 changed files with 131 additions and 3 deletions
+53
View File
@@ -12,10 +12,29 @@ from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
from app.modules.registrations.models import ClientRegistration, RegistrationType from app.modules.registrations.models import ClientRegistration, RegistrationType
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.permission_guard import require_permission from app.modules.core.rbac.permission_guard import require_permission
from app.modules.credential_vault.models import CredentialVaultEntry
from app.modules.credential_vault.service import can_manage_vault, can_open_vault, can_view_entry
router = APIRouter(prefix="/clients", tags=["client-business-structure"]) router = APIRouter(prefix="/clients", tags=["client-business-structure"])
CREDENTIAL_CATEGORY_LABELS = {
"gst_portal": "GST Portal",
"income_tax_portal": "Income Tax Portal",
"traces_tds": "TRACES / TDS",
"mca_portal": "MCA Portal",
"eway_bill": "E-Way Bill",
"einvoice": "E-Invoice",
"government_portal": "Government / Registration Portal",
"banking": "Banking",
"email": "Email",
"software": "Software",
"api_key": "API / Provider",
"digital_signature": "Digital Signature / Token",
"other": "Other",
}
def _tenant_id(request, user): def _tenant_id(request, user):
return int(request.session.get("active_tenant_id") or request.session.get("tenant_id") or user.tenant_id) return int(request.session.get("active_tenant_id") or request.session.get("tenant_id") or user.tenant_id)
@@ -66,11 +85,45 @@ def business_structure_page(request: Request, client_id: int):
RegistrationType.sort_order, RegistrationType.name RegistrationType.sort_order, RegistrationType.name
) )
).scalars().all() ).scalars().all()
# Credential Vault stays the single source of truth. This page only surfaces
# metadata/actions for credentials linked to the registration rows the user can
# already see; encrypted values are never decrypted here.
registration_credentials: dict[int, list[CredentialVaultEntry]] = {row.id: [] for row in registrations}
can_open_credentials = can_open_vault(db, user)
can_manage_credentials = can_manage_vault(db, user)
if registrations and can_open_credentials:
registration_ids = [int(row.id) for row in registrations]
credential_rows = db.execute(
select(CredentialVaultEntry).where(
CredentialVaultEntry.tenant_id == client.tenant_id,
CredentialVaultEntry.client_id == client.id,
CredentialVaultEntry.registration_id.in_(registration_ids),
).order_by(
CredentialVaultEntry.status,
CredentialVaultEntry.category,
CredentialVaultEntry.title,
)
).scalars().all()
raw_active_branch_id = request.session.get("active_branch_id")
active_branch_id = (
int(raw_active_branch_id)
if raw_active_branch_id not in (None, "", 0, "0")
else getattr(user, "branch_id", None)
)
for credential in credential_rows:
if can_view_entry(db, user, credential, active_branch_id):
registration_credentials.setdefault(int(credential.registration_id), []).append(credential)
return templates.TemplateResponse( return templates.TemplateResponse(
"modules/clients/templates/clients/business_structure.html", "modules/clients/templates/clients/business_structure.html",
_context(request, db, user, title="Client Business Structure", client=client, _context(request, db, user, title="Client Business Structure", client=client,
businesses=businesses, branches=branches, registrations=registrations, businesses=businesses, branches=branches, registrations=registrations,
registration_type_codes=registration_type_codes, registration_types=registration_types, registration_type_codes=registration_type_codes, registration_types=registration_types,
registration_credentials=registration_credentials,
credential_category_labels=CREDENTIAL_CATEGORY_LABELS,
can_open_credentials=can_open_credentials,
can_manage_credentials=can_manage_credentials,
can_edit="clients.edit" in set(get_user_permissions(db, user.id))), can_edit="clients.edit" in set(get_user_permissions(db, user.id))),
) )
finally: finally:
@@ -69,9 +69,84 @@
</section> </section>
<section class="rounded-2xl bg-white shadow-soft overflow-hidden"> <section class="rounded-2xl bg-white shadow-soft overflow-hidden">
<div class="border-b p-4 font-semibold">Registrations</div> <div class="flex flex-wrap items-center justify-between gap-2 border-b p-4">
<table class="min-w-full text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Type</th><th class="p-3 text-left">Number</th><th class="p-3 text-left">Trade / Unit Name</th><th class="p-3 text-left">State</th><th class="p-3">Status</th><th></th></tr></thead> <div>
<tbody>{% for row in registrations %}<tr class="border-t"><td class="p-3">{{ registration_type_codes.get(row.id, "-") }}</td><td class="p-3 font-medium">{{ row.registration_number }}</td><td class="p-3">{{ row.trade_name or row.legal_name or '-' }}</td><td class="p-3">{{ row.state or '-' }}</td><td class="p-3 text-center">{{ row.status|replace('_',' ')|title }}</td><td class="p-3">{% if can_edit %}<form method="post" action="/clients/{{ client.id }}/business-structure/registration/{{ row.id }}/toggle"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="text-xs text-brand-700">{{ 'Deactivate' if row.status in ['active','valid','registered'] else 'Activate' }}</button></form>{% endif %}</td></tr>{% else %}<tr><td colspan="6" class="p-6 text-center text-slate-500">No registrations added.</td></tr>{% endfor %}</tbody></table> <div class="font-semibold">Registrations</div>
<div class="mt-0.5 text-xs font-normal text-slate-500">Portal credentials linked to each registration are available here without exposing encrypted values.</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-slate-50">
<tr>
<th class="p-3 text-left">Type</th>
<th class="p-3 text-left">Number</th>
<th class="p-3 text-left">Trade / Unit Name</th>
<th class="p-3 text-left">State</th>
<th class="p-3 text-center">Status</th>
<th class="p-3 text-left">Credentials</th>
<th class="p-3 text-left">Actions</th>
</tr>
</thead>
<tbody>
{% for row in registrations %}
{% set linked_credentials = registration_credentials.get(row.id, []) %}
<tr class="border-t align-top">
<td class="p-3">{{ registration_type_codes.get(row.id, "-") }}</td>
<td class="p-3 font-medium">{{ row.registration_number }}</td>
<td class="p-3">{{ row.trade_name or row.legal_name or '-' }}</td>
<td class="p-3">{{ row.state or '-' }}</td>
<td class="p-3 text-center">{{ row.status|replace('_',' ')|title }}</td>
<td class="p-3 min-w-[260px]">
{% if can_open_credentials %}
{% if linked_credentials %}
<details class="group">
<summary class="cursor-pointer list-none inline-flex items-center gap-2 rounded-lg border border-violet-200 bg-violet-50 px-2.5 py-1.5 text-xs font-semibold text-violet-700">
{{ linked_credentials|length }} credential{{ '' if linked_credentials|length == 1 else 's' }}
<span class="text-[10px] group-open:rotate-180">▼</span>
</summary>
<div class="mt-2 space-y-2">
{% for credential in linked_credentials %}
<div class="rounded-lg border border-slate-200 bg-slate-50 p-2.5">
<div class="flex flex-wrap items-start justify-between gap-2">
<div>
<div class="font-medium text-slate-800">{{ credential.title }}</div>
<div class="text-xs text-slate-500">{{ credential_category_labels.get(credential.category, credential.category|replace('_',' ')|title) }}</div>
</div>
<span class="rounded-full px-2 py-0.5 text-[11px] font-medium {% if credential.status == 'active' %}bg-emerald-100 text-emerald-700{% else %}bg-slate-200 text-slate-600{% endif %}">{{ credential.status|replace('_',' ')|title }}</span>
</div>
<a href="/credential-vault/{{ credential.id }}" class="mt-1.5 inline-block text-xs font-semibold text-brand-700">View credential</a>
</div>
{% endfor %}
</div>
</details>
{% else %}
<span class="text-xs text-slate-400">No credentials added</span>
{% endif %}
{% else %}
<span class="text-xs text-slate-400">Restricted</span>
{% endif %}
</td>
<td class="p-3 min-w-[170px]">
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
{% if can_manage_credentials %}
<a href="/credential-vault/new?client_id={{ client.id }}&registration_id={{ row.id }}" class="text-xs font-semibold text-violet-700">{{ '+ Add Another Credential' if linked_credentials else '+ Add Credential' }}</a>
{% endif %}
{% if can_edit %}
<form method="post" action="/clients/{{ client.id }}/business-structure/registration/{{ row.id }}/toggle">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="text-xs text-brand-700">{{ 'Deactivate' if row.status in ['active','valid','registered'] else 'Activate' }}</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="p-6 text-center text-slate-500">No registrations added.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section> </section>
</div> </div>