from __future__ import annotations from fastapi import APIRouter, Form, Request from fastapi.responses import RedirectResponse from sqlalchemy import select from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf from app.core.security.session_auth import get_current_user from app.core.templating import templates from app.modules.clients.models import Client from app.modules.core.rbac.models import Permission, RolePermission, UserRole from .models import ClientIdentityAuditLog, ClientPortalInvitation from .service import ensure_identity, get_identity, issue_client_invitation, update_login_email router=APIRouter(prefix="/client-identity", tags=["client-identity"]) def _has(db,user,code): return db.execute(select(Permission.id).join(RolePermission,RolePermission.permission_id==Permission.id).join(UserRole,UserRole.role_id==RolePermission.role_id).where(UserRole.user_id==user.id,Permission.code==code,Permission.is_active.is_(True))).first() is not None def _load(db,user,client_id): c=db.get(Client,client_id) if not c or c.tenant_id!=user.tenant_id: return None if user.branch_id and c.branch_id!=user.branch_id and not _has(db,user,"clients.view_all_branches"): return None return c @router.get("/clients/{client_id}") def identity_page(request:Request,client_id:int): db=CommonSessionLocal() try: user=get_current_user(request,db=db) if not user:return RedirectResponse('/login',303) if not _has(db,user,'clients.view'):return RedirectResponse('/clients?error=Access+denied',303) client=_load(db,user,client_id) if not client:return RedirectResponse('/clients?error=Client+not+found',303) identity=get_identity(db,client.id) invites=[]; audits=[] if identity: invites=list(db.execute(select(ClientPortalInvitation).where(ClientPortalInvitation.identity_id==identity.id).order_by(ClientPortalInvitation.id.desc())).scalars().all()) audits=list(db.execute(select(ClientIdentityAuditLog).where(ClientIdentityAuditLog.identity_id==identity.id).order_by(ClientIdentityAuditLog.id.desc()).limit(30)).scalars().all()) return templates.TemplateResponse('client_identity/detail.html',{'request':request,'current_user':user,'csrf_token':get_or_create_csrf_token(request),'client':client,'identity':identity,'invites':invites,'audits':audits,'can_manage':_has(db,user,'clients.edit'),'invite_url':request.session.pop('client_invite_url',None),'flash':request.session.pop('client_identity_flash',None)}) finally:db.close() @router.post("/clients/{client_id}/create") def create_identity(request:Request,client_id:int,email:str=Form(''),csrf_token:str=Form(...)): validate_csrf(request,csrf_token);db=CommonSessionLocal() try: user=get_current_user(request,db=db) if not user or not _has(db,user,'clients.edit'):return RedirectResponse('/clients?error=Access+denied',303) client=_load(db,user,client_id) if not client:return RedirectResponse('/clients?error=Client+not+found',303) try: ensure_identity(db,client=client,actor_user_id=user.id,requested_email=email or None);request.session['client_identity_flash']='Client portal identity created.' except Exception as exc: request.session['client_identity_flash']=str(exc) return RedirectResponse(f'/client-identity/clients/{client_id}',303) finally:db.close() @router.post("/clients/{client_id}/email") def change_email(request:Request,client_id:int,email:str=Form(...),csrf_token:str=Form(...)): validate_csrf(request,csrf_token);db=CommonSessionLocal() try: user=get_current_user(request,db=db) if not user or not _has(db,user,'clients.edit'):return RedirectResponse('/clients?error=Access+denied',303) client=_load(db,user,client_id); identity=get_identity(db,client_id) if client else None try: if not identity: raise ValueError('Create the PAN identity first.') update_login_email(db,identity=identity,email=email,actor_user_id=user.id);request.session['client_identity_flash']='Client portal email updated.' except Exception as exc: request.session['client_identity_flash']=str(exc) return RedirectResponse(f'/client-identity/clients/{client_id}',303) finally:db.close() @router.post("/clients/{client_id}/invite") def invite(request:Request,client_id:int,send_email:str=Form('1'),csrf_token:str=Form(...)): validate_csrf(request,csrf_token);db=CommonSessionLocal() try: user=get_current_user(request,db=db) if not user or not _has(db,user,'clients.edit'):return RedirectResponse('/clients?error=Access+denied',303) client=_load(db,user,client_id); identity=get_identity(db,client_id) if client else None try: if not identity: identity=ensure_identity(db,client=client,actor_user_id=user.id) token,row=issue_client_invitation(db,identity=identity,actor_user_id=user.id,send_email=send_email=='1') request.session['client_invite_url']=f"{str(request.base_url).rstrip('/')}/invite/accept?token={token}" request.session['client_identity_flash']='Invitation issued. Copy the link now; the token is not stored in plain text.' except Exception as exc: request.session['client_identity_flash']=str(exc) return RedirectResponse(f'/client-identity/clients/{client_id}',303) finally:db.close()