Fix invite domain URL and improve shared mobile header
This commit is contained in:
@@ -286,8 +286,31 @@ ONBOARDING_ROLE_CONFIG: dict[str, dict[str, str]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _public_invite_url(invite_token: str) -> str:
|
def _public_invite_url(request, invite_token: str) -> str:
|
||||||
base = (get_settings().ERP_PUBLIC_BASE_URL or "").strip().rstrip("/") or "http://localhost:8000"
|
"""Build an invite URL for the domain currently used by the Firm Admin.
|
||||||
|
|
||||||
|
Tenant custom domains must remain tenant-specific. Prefer the request host
|
||||||
|
(including trusted proxy headers) and retain ERP_PUBLIC_BASE_URL only as a
|
||||||
|
defensive fallback for non-HTTP callers.
|
||||||
|
"""
|
||||||
|
forwarded_proto = (request.headers.get("x-forwarded-proto") or "").split(",", 1)[0].strip().lower()
|
||||||
|
forwarded_host = (request.headers.get("x-forwarded-host") or "").split(",", 1)[0].strip()
|
||||||
|
request_host = (request.headers.get("host") or "").strip()
|
||||||
|
|
||||||
|
scheme = forwarded_proto if forwarded_proto in {"http", "https"} else str(request.url.scheme or "https").lower()
|
||||||
|
host = forwarded_host or request_host or str(request.url.netloc or "").strip()
|
||||||
|
|
||||||
|
# Reject header-control characters before using a host in a generated URL.
|
||||||
|
if host and not any(ch in host for ch in "\r\n/\\"):
|
||||||
|
base = f"{scheme}://{host}".rstrip("/")
|
||||||
|
else:
|
||||||
|
base = str(request.base_url).strip().rstrip("/")
|
||||||
|
|
||||||
|
if not base:
|
||||||
|
base = (get_settings().ERP_PUBLIC_BASE_URL or "").strip().rstrip("/")
|
||||||
|
if not base:
|
||||||
|
raise ValueError("Unable to determine the public ERP URL for the invite link.")
|
||||||
|
|
||||||
return f"{base}/invite/accept?token={invite_token}"
|
return f"{base}/invite/accept?token={invite_token}"
|
||||||
|
|
||||||
|
|
||||||
@@ -498,7 +521,7 @@ def create_firm_internal_user(
|
|||||||
|
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
invite_token = issue_invite_token(db, user)
|
invite_token = issue_invite_token(db, user)
|
||||||
invite_url = _public_invite_url(invite_token)
|
invite_url = _public_invite_url(request, invite_token)
|
||||||
email_status = "not_attempted"
|
email_status = "not_attempted"
|
||||||
email_error = None
|
email_error = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<header class="sticky top-0 z-40 border-b border-slate-200/70 bg-slate-100/85 backdrop-blur-xl">
|
<header class="sticky top-0 z-40 border-b border-slate-200/70 bg-slate-100/85 backdrop-blur-xl">
|
||||||
<div class="mx-auto max-w-[1500px] px-4 py-3 sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-[1500px] px-3 py-3 sm:px-6 lg:px-8">
|
||||||
<div class="rounded-3xl border border-slate-200 bg-white/95 px-4 py-3 shadow-soft">
|
<div class="rounded-3xl border border-slate-200 bg-white/95 px-3 py-3 shadow-soft sm:px-4">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex items-center gap-2 sm:gap-3">
|
||||||
<a href="/workspaces" class="flex min-w-[220px] items-center gap-3 rounded-2xl px-2 py-1 transition hover:bg-slate-50">
|
<a href="/workspaces" class="flex min-w-0 flex-1 items-center gap-2 rounded-2xl px-1 py-1 transition hover:bg-slate-50 sm:gap-3 lg:min-w-[220px] lg:flex-none lg:px-2">
|
||||||
{% if firm_branding.logo_url %}
|
{% if firm_branding.logo_url %}
|
||||||
<img src="{{ firm_branding.logo_url }}" alt="{{ current_firm_name }} logo" class="h-11 w-11 rounded-2xl border border-slate-100 bg-white object-contain p-1 shadow-soft" />
|
<img src="{{ firm_branding.logo_url }}" alt="{{ current_firm_name }} logo" class="h-10 w-10 shrink-0 rounded-2xl border border-slate-100 bg-white object-contain p-1 shadow-soft sm:h-11 sm:w-11" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex h-11 w-11 items-center justify-center rounded-2xl bg-brand-600 text-sm font-bold text-white shadow-soft">{{ (current_firm_name[:2] if current_firm_name else 'AF')|upper }}</div>
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-brand-600 text-sm font-bold text-white shadow-soft sm:h-11 sm:w-11">{{ (current_firm_name[:2] if current_firm_name else 'AF')|upper }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="truncate text-sm font-bold text-slate-900">{{ current_firm_name }}</div>
|
<div class="truncate text-sm font-bold text-slate-900">{{ current_firm_name }}</div>
|
||||||
<div class="truncate text-xs text-slate-500">{{ current_branch_name if active_branch_id else 'All Branches' }}{% if active_financial_year %} • FY {{ active_financial_year }}{% endif %}</div>
|
<div class="truncate text-[11px] text-slate-500 sm:text-xs">{{ current_branch_name if active_branch_id else 'All Branches' }}{% if active_financial_year %} • FY {{ active_financial_year }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
<div class="hidden min-w-0 flex-1 flex-wrap items-center gap-2 lg:flex">
|
||||||
|
{% set workspace_switcher_id = 'workspace_switcher_desktop' %}
|
||||||
{% include "ui/templates/components/workspace_switcher.html" %}
|
{% include "ui/templates/components/workspace_switcher.html" %}
|
||||||
|
|
||||||
{% if full_auth and (can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) or can_view_settings(current_user, ui_perms, ui_roles)) %}
|
{% if full_auth and (can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) or can_view_settings(current_user, ui_perms, ui_roles)) %}
|
||||||
@@ -48,20 +49,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-2">
|
<div class="ml-auto flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||||
<a href="/alerts" class="relative inline-flex h-10 items-center justify-center rounded-2xl border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 shadow-sm hover:border-brand-200 hover:text-brand-700">
|
<a href="/alerts" aria-label="Alerts" class="relative inline-flex h-10 items-center justify-center rounded-2xl border border-slate-200 bg-white px-2.5 text-xs font-semibold text-slate-700 shadow-sm hover:border-brand-200 hover:text-brand-700 sm:px-3">
|
||||||
Alerts
|
<span class="hidden sm:inline">Alerts</span>
|
||||||
{% if unread_alert_count %}<span class="ml-2 rounded-full bg-brand-600 px-2 py-0.5 text-[10px] font-bold text-white">{{ unread_alert_count }}</span>{% endif %}
|
<span class="sm:hidden">🔔</span>
|
||||||
|
{% if unread_alert_count %}<span class="ml-1.5 rounded-full bg-brand-600 px-1.5 py-0.5 text-[10px] font-bold text-white sm:ml-2 sm:px-2">{{ unread_alert_count }}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<details class="group relative">
|
<details class="group relative hidden sm:block">
|
||||||
<summary class="flex cursor-pointer list-none items-center gap-2 rounded-2xl border border-slate-200 bg-white px-2 py-2 shadow-sm hover:border-brand-200">
|
<summary class="flex cursor-pointer list-none items-center gap-2 rounded-2xl border border-slate-200 bg-white px-2 py-2 shadow-sm hover:border-brand-200">
|
||||||
{% if current_user_photo_url %}
|
{% if current_user_photo_url %}
|
||||||
<img src="{{ current_user_photo_url }}" alt="Profile photo" class="h-8 w-8 rounded-xl object-cover" />
|
<img src="{{ current_user_photo_url }}" alt="Profile photo" class="h-8 w-8 rounded-xl object-cover" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-600 text-xs font-bold text-white">{{ current_user_initials }}</div>
|
<div class="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-600 text-xs font-bold text-white">{{ current_user_initials }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="hidden text-left sm:block">
|
<div class="hidden text-left xl:block">
|
||||||
<div class="max-w-[150px] truncate text-xs font-bold text-slate-900">{{ current_user.full_name or current_user.email }}</div>
|
<div class="max-w-[150px] truncate text-xs font-bold text-slate-900">{{ current_user.full_name or current_user.email }}</div>
|
||||||
<div class="max-w-[150px] truncate text-[11px] text-slate-500">{{ current_user.email }}</div>
|
<div class="max-w-[150px] truncate text-[11px] text-slate-500">{{ current_user.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,6 +80,61 @@
|
|||||||
<a class="block rounded-xl px-3 py-2 text-red-600 hover:bg-red-50" href="/logout">Logout</a>
|
<a class="block rounded-xl px-3 py-2 text-red-600 hover:bg-red-50" href="/logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="group relative lg:hidden">
|
||||||
|
<summary aria-label="Open workspace and account menu" class="flex h-10 w-10 cursor-pointer list-none items-center justify-center rounded-2xl border border-slate-200 bg-white text-lg font-bold text-slate-700 shadow-sm hover:border-brand-200 hover:text-brand-700">☰</summary>
|
||||||
|
<div class="fixed inset-x-3 top-[5.4rem] z-50 max-h-[calc(100vh-6.2rem)] overflow-y-auto rounded-3xl border border-slate-200 bg-white p-4 shadow-card sm:absolute sm:inset-x-auto sm:right-0 sm:top-auto sm:mt-2 sm:w-[360px]">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-bold text-slate-900">Workspace & context</div>
|
||||||
|
<div class="text-xs text-slate-500">Change workspace, branch or financial year</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set workspace_switcher_id = 'workspace_switcher_mobile' %}
|
||||||
|
{% include "ui/templates/components/workspace_switcher.html" %}
|
||||||
|
|
||||||
|
{% if full_auth and (can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) or can_view_settings(current_user, ui_perms, ui_roles)) %}
|
||||||
|
<div class="mt-4 grid gap-3">
|
||||||
|
{% if can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) %}
|
||||||
|
{% set mobile_context_tenants = get_context_tenants(request, current_user, ui_perms, ui_roles) %}
|
||||||
|
<label class="grid gap-1 text-xs font-semibold text-slate-600">Audit Firm
|
||||||
|
<select onchange="if(this.value){window.location.href=this.value;}" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700">
|
||||||
|
{% for tenant in mobile_context_tenants %}<option value="/system-settings/context/tenant/{{ tenant.id }}" {% if tenant.id == active_tenant_id %}selected{% endif %}>{{ tenant.name }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) %}
|
||||||
|
{% set mobile_context_branches = get_context_branches(request, current_user, ui_perms, ui_roles) %}
|
||||||
|
<label class="grid gap-1 text-xs font-semibold text-slate-600">Branch
|
||||||
|
<select onchange="if(this.value){window.location.href=this.value;}" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700">
|
||||||
|
<option value="/system-settings/context/branch/0" {% if not active_branch_id %}selected{% endif %}>All Branches</option>
|
||||||
|
{% for branch in mobile_context_branches %}<option value="/system-settings/context/branch/{{ branch.id }}" {% if branch.id == active_branch_id %}selected{% endif %}>{{ branch.name }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
|
||||||
|
{% set mobile_context_financial_years = get_context_financial_years(request, current_user, ui_perms, ui_roles) %}
|
||||||
|
<label class="grid gap-1 text-xs font-semibold text-slate-600">Financial Year
|
||||||
|
<select data-testid="active-fy-select-mobile" aria-label="Active Financial Year" onchange="if(this.value){window.location.href=this.value;}" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700">
|
||||||
|
{% for fy in mobile_context_financial_years %}<option value="/system-settings/context/financial-year/{{ fy.year_code }}" {% if fy.year_code == active_financial_year %}selected{% endif %}>FY {{ fy.year_code }}{% if fy.is_current %} (Current){% endif %}{% if fy.is_locked %} - Locked{% endif %}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4 border-t border-slate-100 pt-3">
|
||||||
|
<div class="mb-2 flex items-center gap-3 rounded-2xl bg-slate-50 p-3">
|
||||||
|
{% if current_user_photo_url %}<img src="{{ current_user_photo_url }}" alt="Profile photo" class="h-9 w-9 rounded-xl object-cover" />{% else %}<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-brand-600 text-xs font-bold text-white">{{ current_user_initials }}</div>{% endif %}
|
||||||
|
<div class="min-w-0"><div class="truncate text-sm font-bold text-slate-900">{{ current_user.full_name or current_user.email }}</div><div class="truncate text-xs text-slate-500">{{ current_user.email }}</div></div>
|
||||||
|
</div>
|
||||||
|
{% if "Consultant" in ui_roles %}<a class="block rounded-xl px-3 py-2.5 text-sm text-slate-700 hover:bg-slate-50" href="/consultant/profile">My Profile</a>{% elif "Client" in ui_roles %}<a class="block rounded-xl px-3 py-2.5 text-sm text-slate-700 hover:bg-slate-50" href="/client/profile">My Profile</a>{% else %}<a class="block rounded-xl px-3 py-2.5 text-sm text-slate-700 hover:bg-slate-50" href="/employee/profile">My Profile</a>{% endif %}
|
||||||
|
<a class="block rounded-xl px-3 py-2.5 text-sm text-slate-700 hover:bg-slate-50" href="/change-password">Change Password</a>
|
||||||
|
<a class="block rounded-xl px-3 py-2.5 text-sm text-red-600 hover:bg-red-50" href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
{% set perms = ui_perms if ui_perms is defined else [] %}
|
{% set perms = ui_perms if ui_perms is defined else [] %}
|
||||||
{% set current = current_path if current_path is defined else request.url.path %}
|
{% set current = current_path if current_path is defined else request.url.path %}
|
||||||
{% set can_employee = ('Staff' in roles) or ('Employee' in roles) or ('employees.ess.view' in perms) or ('employees.work.view_self' in perms) or ('employees.attendance.view_self' in perms) or ('employees.leave.view_self' in perms) or ('employees.documents.view_self' in perms) or ('employees.payroll.view_self' in perms) %}
|
{% set can_employee = ('Staff' in roles) or ('Employee' in roles) or ('employees.ess.view' in perms) or ('employees.work.view_self' in perms) or ('employees.attendance.view_self' in perms) or ('employees.leave.view_self' in perms) or ('employees.documents.view_self' in perms) or ('employees.payroll.view_self' in perms) %}
|
||||||
<div class="mt-2 flex flex-wrap justify-end gap-2">
|
{% set switcher_id = workspace_switcher_id if workspace_switcher_id is defined else 'workspace_switcher' %}
|
||||||
<label class="sr-only" for="workspace_switcher">Workspace</label>
|
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<select id="workspace_switcher" onchange="if(this.value){window.location.href=this.value;}" class="min-w-[230px] max-w-xs rounded-xl border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-900 shadow-sm outline-none transition hover:bg-brand-100 focus:border-brand-400 focus:ring-2 focus:ring-brand-200">
|
<label class="sr-only" for="{{ switcher_id }}">Workspace</label>
|
||||||
|
<select id="{{ switcher_id }}" onchange="if(this.value){window.location.href=this.value;}" class="min-w-0 flex-1 rounded-xl border border-brand-200 bg-brand-50 px-3 py-2 text-xs font-semibold text-brand-900 shadow-sm outline-none transition hover:bg-brand-100 focus:border-brand-400 focus:ring-2 focus:ring-brand-200 sm:min-w-[230px] sm:max-w-xs sm:flex-none">
|
||||||
<option value="/workspaces" {% if current.startswith('/workspaces') %}selected{% endif %}>Workspace Centre</option>
|
<option value="/workspaces" {% if current.startswith('/workspaces') %}selected{% endif %}>Workspace Centre</option>
|
||||||
{% if 'System Admin' in roles %}<option value="/system-admin/dashboard" {% if current.startswith('/system-admin') %}selected{% endif %}>System Admin - Platform Control</option>{% endif %}
|
{% if 'System Admin' in roles %}<option value="/system-admin/dashboard" {% if current.startswith('/system-admin') %}selected{% endif %}>System Admin - Platform Control</option>{% endif %}
|
||||||
{% if 'Partner' in roles %}<option value="/partner/dashboard" {% if current.startswith('/partner') %}selected{% endif %}>Partner Operations</option>{% endif %}
|
{% if 'Partner' in roles %}<option value="/partner/dashboard" {% if current.startswith('/partner') %}selected{% endif %}>Partner Operations</option>{% endif %}
|
||||||
@@ -15,6 +16,6 @@
|
|||||||
{% if 'Client' in roles %}<option value="/client/dashboard" {% if current.startswith('/client') %}selected{% endif %}>Client Portal</option>{% endif %}
|
{% if 'Client' in roles %}<option value="/client/dashboard" {% if current.startswith('/client') %}selected{% endif %}>Client Portal</option>{% endif %}
|
||||||
{% if 'Consultant' in roles %}<option value="/consultant/dashboard" {% if current.startswith('/consultant') %}selected{% endif %}>Consultant Workspace</option>{% endif %}
|
{% if 'Consultant' in roles %}<option value="/consultant/dashboard" {% if current.startswith('/consultant') %}selected{% endif %}>Consultant Workspace</option>{% endif %}
|
||||||
</select>
|
</select>
|
||||||
<a href="/workspaces" class="inline-flex items-center rounded-xl border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 shadow-sm hover:border-brand-200 hover:text-brand-700">All workspaces</a>
|
<a href="/workspaces" class="inline-flex shrink-0 items-center rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-600 shadow-sm hover:border-brand-200 hover:text-brand-700">All workspaces</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user