Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/lib/helpers/http.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios';
import { getUserStore, globalErrorStore, loaderStore, userStore } from '$lib/helpers/store.js';
import { getUserStore, globalErrorStore, loaderStore, userStore, getTenantId } from '$lib/helpers/store.js';
import { renewToken } from '$lib/services/auth-service';
import { delay } from './utils/common';

Expand Down Expand Up @@ -79,6 +79,11 @@ const retryQueue = {
config.headers = config.headers || {};
// @ts-ignore
config.headers.Authorization = `Bearer ${newToken}`;
const tenantId = getTenantId();
if (tenantId) {
// @ts-ignore
config.headers['__tenant'] = tenantId;
}

chain = chain.then(() => delay(this.timeout))
.then(() => {
Expand All @@ -102,12 +107,17 @@ 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();
Expand Down
74 changes: 74 additions & 0 deletions src/lib/helpers/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const conversationKey = "conversation";
const conversationUserStatesKey = "conversation_user_states";
const conversationSearchOptionKey = "conversation_search_option";
const conversationUserMessageKey = "conversation_user_messages";
const tenantKey = "tenant_id";
const tenantNameKey = "tenant_name";

/** @type {Writable<import('$commonTypes').GlobalEvent>} */
const createGlobalEventStore = () => {
Expand Down Expand Up @@ -53,6 +55,76 @@ export function getUserStore() {
}
};


/** @returns {string} */
export function getTenantId() {
if (!browser) return '';
return sessionStorage.getItem(tenantKey) || '';
}

/** @param {string} tenantId */
export function setTenantId(tenantId) {
if (!browser) return;
if (!tenantId) {
sessionStorage.removeItem(tenantKey);
return;
}
sessionStorage.setItem(tenantKey, tenantId);
}

export function clearTenantId() {
if (!browser) return;
sessionStorage.removeItem(tenantKey);
}

/** @returns {string} */
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);
} else {
sessionStorage.setItem(tenantNameKey, tenantName);
}

if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('tenantChanged', {
detail: {
tenantId: getTenantId(),
tenantName: getTenantName()
}
}));
}
}

export function clearTenantName() {
if (!browser) return;
sessionStorage.removeItem(tenantNameKey);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('tenantChanged', {
detail: {
tenantId: getTenantId(),
tenantName: ''
}
}));
}
}

export function notifyTenantChanged() {
if (!browser || typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent('tenantChanged', {
detail: {
tenantId: getTenantId(),
tenantName: getTenantName()
}
}));
}

userStore.subscribe(value => {
if (browser && value.token) {
sessionStorage.setItem(userKey, JSON.stringify(value));
Expand Down Expand Up @@ -228,6 +300,8 @@ export function resetStorage(resetUser = false) {

if (resetUser) {
sessionStorage.removeItem(userKey);
sessionStorage.removeItem(tenantKey);
sessionStorage.removeItem(tenantNameKey);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/services/api-endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const endpoints = {
userUpdateUrl: `${host}/user`,
usrCreationUrl: `${host}/user`,
userAvatarUrl: `${host}/user/avatar`,

//tenant
userTenantsUrl: `${host}/tenants/options`,

// setting
settingListUrl: `${host}/settings`,
Expand Down
44 changes: 41 additions & 3 deletions src/lib/services/auth-service.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { userStore, getUserStore } from '$lib/helpers/store.js';
import { userStore, getUserStore, resetStorage, setTenantId, clearTenantId, getTenantId, clearTenantName, notifyTenantChanged } from '$lib/helpers/store.js';
import { endpoints } from './api-endpoints.js';
import axios from 'axios';

/**
* @param {string} email
* @param {string} password
* @param {string} tenantId
* @param {function} onSucceed
* @param {function} onError
*/
export async function getToken(email, password, onSucceed, onError) {
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,
Expand All @@ -34,6 +40,15 @@ export async function getToken(email, password, onSucceed, onError) {
user.expires = result.expires;
user.renew_token_count = 0;
userStore.set(user);

if (tenantId) {
setTenantId(tenantId);
notifyTenantChanged();
} else {
clearTenantId();
clearTenantName();
}

onSucceed();
})
.catch(() => {
Expand All @@ -47,11 +62,13 @@ export async function getToken(email, password, onSucceed, onError) {
* @param {(() => void) | null} [onError]
*/
export async function renewToken(token, onSucceed = null, onError = null) {
const tenantId = getTenantId();
await fetch(endpoints.renewTokenUrl, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
"Authorization": `Bearer ${token}`,
...(tenantId ? { "__tenant": tenantId } : {})
},
body: JSON.stringify({ refresh_token: token, access_token: token }),
}).then(response => {
Expand Down Expand Up @@ -137,4 +154,25 @@ export async function register(firstName, lastName, email, password, onSucceed)
export async function uploadUserAvatar(file) {
const response = await axios.post(endpoints.userAvatarUrl, { ...file });
return response?.data;
}

export async function getTenantOptions() {
try {
const response = await fetch(`${endpoints.userTenantsUrl}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();

return result;
} catch (error) {
return null;
}
}
82 changes: 78 additions & 4 deletions src/routes/(authentication)/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Alert
} from '@sveltestrap/sveltestrap';
import Headtitle from '$lib/common/HeadTitle.svelte';
import { getToken } from '$lib/services/auth-service.js';
import { getToken, getTenantOptions } from '$lib/services/auth-service.js';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
Expand All @@ -31,7 +31,7 @@
PUBLIC_AUTH_ENABLE_FIND_PWD,
} from '$env/static/public';
import { onMount } from 'svelte';
import { resetStorage } from '$lib/helpers/store';
import { resetStorage, setTenantName, clearTenantName } from '$lib/helpers/store';

let username = PUBLIC_ADMIN_USERNAME;
let password = PUBLIC_ADMIN_PASSWORD;
Expand All @@ -40,13 +40,20 @@
let status = '';
let isSubmitting = false;
let isRememberMe = false;
let tenantId = '';
/** @type {{ tenantId: string, name: string }[]} */
let tenantOptions = [];
let tenantOptionsLoaded = false;

onMount(() => {
onMount(async () => {
const userName = localStorage.getItem('user_name');
isRememberMe = userName !== null;
if(isRememberMe){
username = userName || '';
}

// Load tenant options when opening the login page
await getTenamtOptions();
});
function handleRememberMe(){
if(isRememberMe){
Expand All @@ -62,10 +69,29 @@
isSubmitting = true;
handleRememberMe();
e.preventDefault();
await getToken(username, password, () => {

// 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();
}

const redirectUrl = $page.url.searchParams.get('redirect');
isSubmitting = false;
resetStorage();
Expand All @@ -88,6 +114,34 @@
isSubmitting = false;
}

async function getTenamtOptions() {
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) {
tenantId = '';
} else if (tenantOptions.length === 1) {
tenantId = tenantOptions[0].tenantId;
} else {
// keep current selection if still valid
const stillValid = tenantOptions.some((x) => x.tenantId === tenantId);
if (!stillValid) tenantId = '';
}
} catch (error) {
tenantOptions = [];
tenantId = '';
} finally {
tenantOptionsLoaded = true;
}
}

function onPasswordToggle() {
const x = document.getElementById('user-password');
if (!x) return;
Expand Down Expand Up @@ -143,6 +197,26 @@
<div class="p-2">
<Alert {isOpen} color={status}>{msg}</Alert>
<Form class="form-horizontal" on:submit={onSubmit}>
{#if tenantOptions.length > 0}
<div class="mb-3">
<Label for="tenant" class="form-label">Tenant</Label>
<Input
type="select"
class="form-select"
id="tenant"
disabled={isSubmitting}
bind:value={tenantId}
>
{#if tenantOptions.length > 1}
<option value="" disabled selected={tenantId === ''}>Please select</option>
{/if}
{#each tenantOptions as t (t.tenantId)}
<option value={t.tenantId}>{t.name}</option>
{/each}
</Input>
</div>
{/if}

<div class="mb-3">
<Label for="username" class="form-label">Username</Label>
<Input
Expand Down
Loading