Skip to content

Conversation

@tracyma-05
Copy link

@tracyma-05 tracyma-05 commented Jan 5, 2026

User description

User description

Added multi-tenant UX support to BotSharp-UI. When multi-tenancy is enabled, the login flow loads a selectable tenant list from the BotSharp backend and sends the selected tenant id via __tenant request header. After login, the selected tenant name is displayed in the top-right user area to make the active tenant context explicit.

PR Type

Enhancement, UI

Description

  • Login UX

    • Fetch tenant options from backend: GET /tenants/options
    • Render a tenant selector on the login page when options are available
    • Require tenant selection before submitting login
    • Include selected tenant id in all auth-related requests (and subsequent API calls) via __tenant header
  • Post-login UX

    • Persist selected tenant id/name in client-side state (e.g., store/state manager)
    • Display current tenant name in the top-right header area (near user avatar/menu)
  • Backward compatibility

    • When multi-tenancy is disabled on server, /tenants/options returns empty array and UI falls back to original single-tenant login without tenant selector
    • If tenant options request fails, UI should either show a non-blocking error or gracefully fall back (depending on existing UX pattern)

Backend dependency

  • Tenant options endpoint

    • GET /tenants/options
    • Response: [{ tenantId: string, name: string }]
  • Tenant header

    • Request header: __tenant: <tenantId>
    • The server resolves current tenant from the header (and scopes it via middleware) before executing downstream services

Flow Walkthrough

sequenceDiagram
  participant UI as BotSharp-UI
  participant API as BotSharp API

  UI->>API: GET /tenants/options
  API-->>UI: [{tenantId, name}] or []

  alt multi-tenant enabled (non-empty)
    UI->>UI: user selects tenant
    UI->>API: POST /auth/login with header __tenant: <tenantId>
    API-->>UI: token / session
    UI->>UI: persist token + tenantName
    UI->>API: subsequent API calls with header __tenant: <tenantId>
    UI->>UI: show tenantName in top-right header
  else disabled / empty list
    UI->>API: POST /auth/login (original payload)
    API-->>UI: token / session
  end
Loading

Testing

  • Multi-tenant enabled

    • Login page shows tenant selector
    • Selecting tenant and logging in sends __tenant header
    • After login, header shows tenant name
  • Multi-tenant disabled

    • Tenant selector not shown
    • Login behavior unchanged
  • Edge cases

    • /tenants/options returns empty: treat as single-tenant
    • /tenants/options fails: fallback behavior matches existing UI error handling

PR Type

Enhancement


Description

  • Add multi-tenant support to login flow with tenant selector

  • Persist tenant ID/name in session storage and display in header

  • Include tenant ID in all API requests via __tenant header

  • Gracefully handle single-tenant and multi-tenant scenarios


Diagram Walkthrough

flowchart LR
  A["Login Page"] -->|GET /tenants/options| B["Backend API"]
  B -->|Tenant List| C["Render Tenant Selector"]
  C -->|User Selects Tenant| D["Submit Login with __tenant Header"]
  D -->|POST /auth/login| B
  B -->|Auth Token| E["Store Tenant ID/Name"]
  E -->|Display in Header| F["Header Component"]
  G["All API Requests"] -->|Include __tenant Header| B
Loading

File Walkthrough

Relevant files
Enhancement
+page.svelte
Add tenant selector and multi-tenant login flow                   

src/routes/(authentication)/login/+page.svelte

  • Import getTenantOptions from auth service and tenant store helpers
  • Add tenant selector dropdown UI that renders when tenant options
    available
  • Fetch tenant options on component mount and before form submission
  • Validate tenant selection before allowing login when multi-tenancy
    enabled
  • Pass selected tenant ID to getToken() and persist tenant name after
    successful login
+78/-4   
Header.svelte
Display current tenant name in header                                       

src/routes/VerticalLayout/Header.svelte

  • Import getTenantName from store helpers
  • Add tenant name state variable and initialize on mount
  • Listen to tenantChanged custom event to update displayed tenant name
  • Display current tenant name in header near language/notification
    dropdowns
+19/-1   
http.js
Include tenant ID in all HTTP requests                                     

src/lib/helpers/http.js

  • Import getTenantId from store helpers
  • Add __tenant header to all axios requests when tenant ID is available
  • Include tenant header in token retry queue when renewing
    authentication
+11/-1   
store.js
Add tenant state management functions                                       

src/lib/helpers/store.js

  • Add tenantKey and tenantNameKey constants for session storage
  • Implement getTenantId(), setTenantId(), clearTenantId() functions
  • Implement getTenantName(), setTenantName(), clearTenantName()
    functions
  • Add notifyTenantChanged() to dispatch custom event for tenant changes
  • Update resetStorage() to clear tenant data when resetting user session
+74/-0   
auth-service.js
Add tenant parameter to auth functions                                     

src/lib/services/auth-service.js

  • Update getToken() to accept tenantId parameter and include in auth
    headers
  • Store tenant ID in session storage after successful login
  • Dispatch tenant changed event after authentication
  • Update renewToken() to include __tenant header when tenant ID exists
  • Add new getTenantOptions() function to fetch available tenants from
    backend
+41/-3   
Configuration changes
api-endpoints.js
Add tenant options API endpoint                                                   

src/lib/services/api-endpoints.js

  • Add userTenantsUrl endpoint constant pointing to /tenants/options
+3/-0     

@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Misspelled function name: The new function name getTenamtOptions() is misspelled, reducing readability and
increasing the chance of future mistakes.

Referred Code
	await getTenamtOptions();
});
function handleRememberMe(){
	if(isRememberMe){
		localStorage.setItem("user_name", username);
	}
	else {
		localStorage.removeItem("user_name");
	}
}

/** @param {any} e */
async function onSubmit(e) {
	isSubmitting = true;
	handleRememberMe();
	e.preventDefault();

	// Ensure tenant options have been fetched at least once
	if (!tenantOptionsLoaded) {
		await getTenamtOptions();
	}


 ... (clipped 67 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent error paths: Tenant option fetch and missing-tenant selection failures are handled silently (catch
clears state; submit returns early) without actionable user feedback or logging for
diagnosis.

Referred Code
// Ensure tenant options have been fetched at least once
if (!tenantOptionsLoaded) {
	await getTenamtOptions();
}

if (tenantOptions?.length > 0 && !tenantId) {
	isSubmitting = false;
	return;
}

await getToken(username, password, tenantOptions?.length > 0 ? tenantId : '', () => {
	isOpen = true;
	msg = 'Authentication success';
	status = 'success';

	if (tenantOptions?.length > 0) {
			const selected = tenantOptions.find((x) => x.tenantId === tenantId);
			setTenantName(selected?.name || '');
		} else {
			clearTenantName();
		}


 ... (clipped 49 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: The PR introduces tenant-scoped authentication behavior but the diff contains no explicit
audit logging for login/tenant-selection events, which may be handled elsewhere outside
the visible UI code.

Referred Code
export async function getToken(email, password, tenantId, onSucceed, onError) {
    const credentials = btoa(`${email}:${password}`);
    /** @type {Record<string, string>} */
    const headers = {
        Authorization: `Basic ${credentials}`,
    };

    if (tenantId) {
        headers['__tenant'] = tenantId;
    }

    await fetch(endpoints.tokenUrl, {
        method: 'POST',
        headers: headers,
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            console.log(response.statusText);
            onError();
            return false;


 ... (clipped 20 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Tenant ID trust: The PR adds the __tenant header to all requests from a client-side stored value, which
requires confirmation that the backend strictly validates authorization for the tenant and
does not trust the header alone.

Referred Code
axios.interceptors.request.use(
    (config) => {
        // Add your authentication logic here
        const user = getUserStore();
        const tenantId = getTenantId();
        if (!skipLoader(config)) {
            loaderStore.set(true);
        }
        // Attach an authentication token to the request headers
        if (user.token) {
            config.headers.Authorization = `Bearer ${user.token}`;

            if (tenantId) {
                config.headers['__tenant'] = tenantId;
            }
        } else {
            retryQueue.queue = [];
            redirectToLogin();
        }
        return config;

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Use Svelte stores for tenant state

Refactor the tenant state management to use a Svelte writable store instead of
direct sessionStorage access and global window events. This change would align
with the application's existing architecture, centralize state logic, and
improve reactivity.

Examples:

src/lib/helpers/store.js [81-103]
export function getTenantName() {
    if (!browser) return '';
    return sessionStorage.getItem(tenantNameKey) || '';
}

/** @param {string} tenantName */
export function setTenantName(tenantName) {
    if (!browser) return;
    if (!tenantName) {
        sessionStorage.removeItem(tenantNameKey);

 ... (clipped 13 lines)
src/routes/VerticalLayout/Header.svelte [28-35]
    onMount(() => {
        tenantName = getTenantName();
        const handler = (/** @type {any} */ e) => {
            tenantName = e?.detail?.tenantName || getTenantName() || '';
        };
        window.addEventListener('tenantChanged', handler);
        return () => window.removeEventListener('tenantChanged', handler);
    });

Solution Walkthrough:

Before:

// src/lib/helpers/store.js
export function getTenantName() {
    if (!browser) return '';
    return sessionStorage.getItem(tenantNameKey) || '';
}

export function setTenantName(tenantName) {
    // ... sets sessionStorage
    if (typeof window !== 'undefined') {
        window.dispatchEvent(new CustomEvent('tenantChanged', ...));
    }
}

// src/routes/VerticalLayout/Header.svelte
let tenantName = '';
onMount(() => {
    tenantName = getTenantName();
    window.addEventListener('tenantChanged', (e) => {
        tenantName = e?.detail?.tenantName || '';
    });
});

After:

// src/lib/helpers/store.js
const createTenantStore = () => {
  const { subscribe, set } = writable({ id: getTenantId(), name: getTenantName() });

  return {
    subscribe,
    set: (value) => {
      setTenantId(value.id);
      setTenantName(value.name);
      set(value);
    }
    // ... other methods
  };
};
export const tenantStore = createTenantStore();

// src/routes/VerticalLayout/Header.svelte
import { tenantStore } from '$lib/helpers/store';
// ... in template
{#if $tenantStore.name}
  <span class="... text-muted">Tenant: {$tenantStore.name}</span>
{/if}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant architectural inconsistency; the PR introduces a manual state management system using sessionStorage and global events, which is contrary to the established Svelte store pattern already used in the application, impacting maintainability and code quality.

Medium
Possible issue
Fix typo in function definition

Correct the typo in the function definition from getTenamtOptions to
getTenantOptions.

src/routes/(authentication)/login/+page.svelte [117-130]

-async function getTenamtOptions() {
+async function getTenantOptions() {
 		try {
 			let data = await getTenantOptions();
 			const raw = Array.isArray(data) ? data : [];
 			tenantOptions = raw
 				.map((/** @type {any} */ x) => ({
 					tenantId: x?.tenantId || x?.id || '',
 					name: x?.name || x?.tenantName || x?.displayName || x?.id || x?.tenantId || ''
 				}))
 				.filter((/** @type {{tenantId: string}} */ x) => !!x.tenantId);
 
 			if (tenantOptions.length === 0) {
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a typo in the function definition getTenamtOptions which would cause a runtime error. Fixing this critical bug is important for the feature to work correctly.

Medium
Add tenant selection validation

Add user feedback when tenant selection is required but a tenant has not been
selected.

src/routes/(authentication)/login/+page.svelte [78-81]

 if (tenantOptions?.length > 0 && !tenantId) {
   isSubmitting = false;
+  msg = 'Please select a tenant';
+  status = 'danger';
+  isOpen = true;
   return;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion improves user experience by providing feedback when a required tenant is not selected, preventing a silent failure. This is a valuable enhancement to the login flow.

Medium
  • More

@iceljc iceljc merged commit b450472 into SciSharp:main Jan 7, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants