diff --git a/.scripts/configure.ts b/.scripts/configure.ts index ddc676d7deb..d9df0da12b5 100644 --- a/.scripts/configure.ts +++ b/.scripts/configure.ts @@ -78,6 +78,7 @@ if (!isDocker) { API_BASE_URL: API_BASE_URL, CLIENT_BASE_URL: CLIENT_BASE_URL, + COOKIE_DOMAIN: '${env.COOKIE_DOMAIN}', PLATFORM_WEBSITE_URL: '${env.PLATFORM_WEBSITE_URL}', PLATFORM_WEBSITE_DOWNLOAD_URL: '${env.PLATFORM_WEBSITE_DOWNLOAD_URL}', @@ -221,6 +222,7 @@ if (!isDocker) { API_BASE_URL: API_BASE_URL, CLIENT_BASE_URL: CLIENT_BASE_URL, + COOKIE_DOMAIN: 'DOCKER_COOKIE_DOMAIN', PLATFORM_WEBSITE_URL: 'DOCKER_PLATFORM_WEBSITE_URL', PLATFORM_WEBSITE_DOWNLOAD_URL: 'DOCKER_PLATFORM_WEBSITE_DOWNLOAD_URL', diff --git a/.scripts/env.ts b/.scripts/env.ts index 4303ff0a472..d010c380ec7 100644 --- a/.scripts/env.ts +++ b/.scripts/env.ts @@ -10,6 +10,8 @@ export type Env = Readonly<{ // Set to true if build / runs in Docker IS_DOCKER: boolean; + COOKIE_DOMAIN: string; + // Base URL of Gauzy UI website CLIENT_BASE_URL: string; @@ -140,6 +142,8 @@ export const env: Env = cleanEnv( IS_DOCKER: bool({ default: false }), + COOKIE_DOMAIN: str({ default: '.gauzy.co' }), + CLIENT_BASE_URL: str({ default: 'http://localhost:4200' }), API_BASE_URL: str({ default: 'http://localhost:3000' }), diff --git a/apps/gauzy/src/app/app.module.guard.ts b/apps/gauzy/src/app/app.module.guard.ts index 915c37794ac..aef68709acf 100644 --- a/apps/gauzy/src/app/app.module.guard.ts +++ b/apps/gauzy/src/app/app.module.guard.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router'; +import { Router, ActivatedRouteSnapshot } from '@angular/router'; import { environment } from '@gauzy/ui-config'; import { Store } from '@gauzy/ui-core/core'; @Injectable() -export class AppModuleGuard implements CanActivate { +export class AppModuleGuard { constructor(private readonly router: Router, private readonly store: Store) {} /** diff --git a/apps/gauzy/src/app/pages/invoices/invoice-edit/invoice-edit.component.ts b/apps/gauzy/src/app/pages/invoices/invoice-edit/invoice-edit.component.ts index e7ee22c543a..2905ed19d55 100644 --- a/apps/gauzy/src/app/pages/invoices/invoice-edit/invoice-edit.component.ts +++ b/apps/gauzy/src/app/pages/invoices/invoice-edit/invoice-edit.component.ts @@ -311,9 +311,8 @@ export class InvoiceEditComponent extends PaginationFilterBaseComponent implemen component: InvoiceProductsSelectorComponent }, valuePrepareFunction: (product: IProduct) => { - return product?.name - ? `${this.translatableService.getTranslatedProperty(product, 'name')}` - : ''; + const translatedName = this.translatableService.getTranslatedProperty(product, 'name'); + return translatedName || ''; } }; break; diff --git a/apps/gauzy/src/app/pages/pages.component.ts b/apps/gauzy/src/app/pages/pages.component.ts index cc12f8a93ca..b8c2facdba0 100644 --- a/apps/gauzy/src/app/pages/pages.component.ts +++ b/apps/gauzy/src/app/pages/pages.component.ts @@ -2,12 +2,10 @@ import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Data, Router } from '@angular/router'; import { NbMenuItem } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; -import { merge, pairwise } from 'rxjs'; -import { filter, map, take, tap } from 'rxjs/operators'; +import { filter, map, merge, pairwise, take, tap } from 'rxjs'; import { NgxPermissionsService } from 'ngx-permissions'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { chain } from 'underscore'; -import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; +import { FeatureEnum, IOrganization, IRolePermission, IUser, IntegrationEnum, PermissionsEnum } from '@gauzy/contracts'; import { AuthStrategy, IJobMatchingEntity, @@ -19,8 +17,8 @@ import { Store, UsersService } from '@gauzy/ui-core/core'; -import { FeatureEnum, IOrganization, IRolePermission, IUser, IntegrationEnum, PermissionsEnum } from '@gauzy/contracts'; import { distinctUntilChange, isNotEmpty } from '@gauzy/ui-core/common'; +import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; import { ReportService } from './reports/all-report/report.service'; @UntilDestroy({ checkProperties: true }) @@ -80,15 +78,9 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie filter((organization: IOrganization) => !!organization), distinctUntilChange(), pairwise(), // Pair each emitted value with the previous one - tap(([organization]: [IOrganization, IOrganization]) => { - const { id: organizationId, tenantId } = organization; - + tap(([previousOrganization]: [IOrganization, IOrganization]) => { // Remove the specified menu items for previous selected organization - this._navMenuBuilderService.removeNavMenuItems( - // Define the base item IDs - this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organizationId}-${tenantId}`), - 'reports' - ); + this.removeOrganizationReportsMenuItems(previousOrganization); }), untilDestroyed(this) ) @@ -118,25 +110,21 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie .subscribe(); this.reportService.menuItems$.pipe(distinctUntilChange(), untilDestroyed(this)).subscribe((menuItems) => { - if (menuItems) { - this.reportMenuItems = chain(menuItems) - .values() - .map((item) => { - return { - id: item.slug + `-${this.organization?.id}`, - title: item.name, - link: `/pages/reports/${item.slug}`, - icon: item.iconClass, - data: { - translationKey: `${item.name}` - } - }; - }) - .value(); - } else { - this.reportMenuItems = []; - } - this.addOrganizationReportsMenuItems(); + // Convert the menuItems object to an array + const reportItems = menuItems ? Object.values(menuItems) : []; + + this.reportMenuItems = reportItems.map((item) => ({ + id: item.slug, + title: item.name, + link: `/pages/reports/${item.slug}`, + icon: item.iconClass, + data: { + translationKey: item.name + } + })); + + // Add the report menu items to the navigation menu + this.addOrRemoveOrganizationReportsMenuItems(); }); } @@ -176,49 +164,62 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie } /** - * Adds report menu items to the organization's navigation menu. + * Removes the report menu items associated with the current organization. + * + * This function checks if the organization is defined. If not, it logs a warning and exits early. + * If the organization is defined, it constructs item IDs based on the organization and tenant ID + * and removes these items from the navigation menu. + * + * @returns {void} This function does not return a value. */ - private addOrganizationReportsMenuItems() { - if (!this.organization) { - // Handle the case where this.organization is not defined - console.warn('Organization not defined. Unable to add/remove menu items.'); + private removeOrganizationReportsMenuItems(organization: IOrganization): void { + // Return early if the organization is not defined, logging a warning + if (!organization) { + console.warn(`Organization not defined. Unable to remove menu items.`); return; } - const { id: organizationId, tenantId } = this.organization; - // Remove the specified menu items for current selected organization - // Note: We need to remove old menus before constructing new menus for the organization. - this._navMenuBuilderService.removeNavMenuItems( - // Define the base item IDs - this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organizationId}-${tenantId}`), - 'reports' + // Destructure organization properties + const { id: organizationId, tenantId } = organization; + + // Generate the item IDs to remove and call the service method + const itemIdsToRemove = this.getReportMenuBaseItemIds().map( + (itemId) => `${itemId}-${organizationId}-${tenantId}` ); - // Validate if reportMenuItems is an array and has elements - if (!Array.isArray(this.reportMenuItems) || this.reportMenuItems.length === 0) { + this._navMenuBuilderService.removeNavMenuItems(itemIdsToRemove, 'reports'); + } + + /** + * Adds report menu items to the organization's navigation menu. + */ + private addOrRemoveOrganizationReportsMenuItems() { + if (!this.organization) { + console.warn('Organization not defined. Unable to add/remove menu items.'); return; } + const { id: organizationId, tenantId } = this.organization; + + // Remove old menu items before constructing new ones for the organization + this.removeOrganizationReportsMenuItems(this.organization); + // Iterate over each report and add it to the navigation menu - try { - this.reportMenuItems.forEach((report: NavMenuSectionItem) => { - // Validate the structure of each report item - if (report && report.id && report.title) { - this._navMenuBuilderService.addNavMenuItem( - { - id: report.id, // Unique identifier for the menu item - title: report.title, // The title of the menu item - icon: report.icon, // The icon class for the menu item, using FontAwesome in this case - link: report.link, // The link where the menu item directs - data: report.data - }, - 'reports' - ); // The id of the section where this item should be added - } - }); - } catch (error) { - console.error('Error adding report menu items', error); - } + this.reportMenuItems.forEach((report: NavMenuSectionItem) => { + // Validate the structure of each report item + if (report?.id && report?.title) { + this._navMenuBuilderService.addNavMenuItem( + { + id: `${report.id}-${organizationId}-${tenantId}`, // Unique identifier for the menu item + title: report.title, // The title of the menu item + icon: report.icon, // The icon class for the menu item + link: report.link, // The link where the menu item directs + data: report.data // The data associated with the menu item + }, + 'reports' // The id of the section where this item should be added + ); + } + }); } /** @@ -402,5 +403,8 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie this.store.featureTenant = tenant.featureOrganizations.filter((item) => !item.organizationId); } - ngOnDestroy() {} + ngOnDestroy() { + // Remove the report menu items associated with the current organization before destroying the component + this.removeOrganizationReportsMenuItems(this.organization); + } } diff --git a/packages/ui-config/src/lib/environments/model.ts b/packages/ui-config/src/lib/environments/model.ts index bece11decb0..41960b5ebd2 100644 --- a/packages/ui-config/src/lib/environments/model.ts +++ b/packages/ui-config/src/lib/environments/model.ts @@ -3,6 +3,7 @@ export interface Environment { API_BASE_URL: string; CLIENT_BASE_URL: string; + COOKIE_DOMAIN?: string; PLATFORM_WEBSITE_URL?: string; PLATFORM_WEBSITE_DOWNLOAD_URL?: string; diff --git a/packages/ui-core/common/src/lib/constants/index.ts b/packages/ui-core/common/src/lib/constants/index.ts index fff2f7aa02a..98f36d229c5 100644 --- a/packages/ui-core/common/src/lib/constants/index.ts +++ b/packages/ui-core/common/src/lib/constants/index.ts @@ -1,3 +1,4 @@ export * from './app.constants'; export * from './layout.constants'; +export * from './route.constant'; export * from './timesheet.constants'; diff --git a/packages/ui-core/common/src/lib/constants/route.constant.ts b/packages/ui-core/common/src/lib/constants/route.constant.ts new file mode 100644 index 00000000000..6bd97cfca14 --- /dev/null +++ b/packages/ui-core/common/src/lib/constants/route.constant.ts @@ -0,0 +1,4 @@ +// In a constants file or configuration service +export const ROUTES = { + DASHBOARD: '/pages/dashboard' +} as const; diff --git a/packages/ui-core/core/src/index.ts b/packages/ui-core/core/src/index.ts index b6be0396738..38d20e147f1 100644 --- a/packages/ui-core/core/src/index.ts +++ b/packages/ui-core/core/src/index.ts @@ -5,6 +5,7 @@ export * from './lib/auth'; export * from './lib/common/component-registry.types'; export * from './lib/components'; export * from './lib/core.module'; +export * from './lib/extension'; export * from './lib/guards'; export * from './lib/interceptors'; export * from './lib/module-import-guard'; diff --git a/packages/ui-core/core/src/lib/auth/auth.guard.ts b/packages/ui-core/core/src/lib/auth/auth.guard.ts index 071cd7f8562..14802e45f08 100644 --- a/packages/ui-core/core/src/lib/auth/auth.guard.ts +++ b/packages/ui-core/core/src/lib/auth/auth.guard.ts @@ -1,16 +1,17 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { AuthService, AuthStrategy, ElectronService, Store } from '../services'; +import { getCookie } from './cookie-helper'; @Injectable() -export class AuthGuard implements CanActivate { +export class AuthGuard { constructor( - private readonly router: Router, - private readonly authService: AuthService, - private readonly authStrategy: AuthStrategy, - private readonly store: Store, - private readonly electronService: ElectronService + private readonly _router: Router, + private readonly _authService: AuthService, + private readonly _authStrategy: AuthStrategy, + private readonly _store: Store, + private readonly _electronService: ElectronService ) {} /** @@ -21,20 +22,30 @@ export class AuthGuard implements CanActivate { * @return {Promise} A promise that resolves to true if the user is authenticated, false otherwise. */ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - const token = route.queryParamMap.get('token'); - const userId = route.queryParamMap.get('userId'); + const token = route.queryParamMap.get('token') || getCookie('token'); + const userId = route.queryParamMap.get('userId') || getCookie('userId'); + const refreshToken = route.queryParamMap.get('refresh_token') || getCookie('refresh_token'); + // If token and userId exist, store them if (token && userId) { - this.store.token = token; - this.store.userId = userId; + this._store.token = token; + this._store.userId = userId; + this._store.refresh_token = refreshToken; } - if (await this.authService.isAuthenticated()) { - // Logged in, so allow navigation - return true; + // Validate the token before proceeding + if (token && !this.validateToken(token)) { + console.error('Invalid token, redirecting to login page...'); + await this.handleLogout(state.url); // Handle invalid token + return false; // Prevent navigation } - // Not logged in, handle the logout process + // Check if the user is authenticated + if (await this._authService.isAuthenticated()) { + return true; // Allow navigation + } + + // Not authenticated, handle logout await this.handleLogout(state.url); return false; } @@ -45,15 +56,25 @@ export class AuthGuard implements CanActivate { * @param {string} returnUrl - The URL to return to after logging in. */ private async handleLogout(returnUrl: string): Promise { - if (this.electronService.isElectron) { + if (this._electronService.isElectron) { try { - this.electronService.ipcRenderer.send('logout'); + this._electronService.ipcRenderer.send('logout'); } catch (error) { console.error('Error sending logout message to Electron:', error); } } - await firstValueFrom(this.authStrategy.logout()); - await this.router.navigate(['/auth/login'], { queryParams: { returnUrl } }); + await firstValueFrom(this._authStrategy.logout()); + await this._router.navigate(['/auth/login'], { queryParams: { returnUrl } }); + } + + /** + * Validates the format of a JWT token. + * + * @param {string} token - The JWT token to validate. + * @returns {boolean} - Returns true if the token is valid, otherwise false. + */ + private validateToken(token: string): boolean { + return typeof token === 'string' && token.trim().length > 0 && token.split('.').length === 3; } } diff --git a/packages/ui-core/core/src/lib/auth/auth.module.ts b/packages/ui-core/core/src/lib/auth/auth.module.ts index be9ed870a50..d80b7c158f3 100644 --- a/packages/ui-core/core/src/lib/auth/auth.module.ts +++ b/packages/ui-core/core/src/lib/auth/auth.module.ts @@ -6,6 +6,9 @@ import { AuthGuard } from './auth.guard'; import { NoAuthGuard } from './no-auth.guard'; import { AuthService, AuthStrategy, ElectronService, Store } from '../services'; +/** + * Social links for auth + */ const socialLinks = [ { url: environment.GOOGLE_AUTH_LINK, diff --git a/packages/ui-core/core/src/lib/auth/cookie-helper.ts b/packages/ui-core/core/src/lib/auth/cookie-helper.ts new file mode 100644 index 00000000000..be0a3cc28d9 --- /dev/null +++ b/packages/ui-core/core/src/lib/auth/cookie-helper.ts @@ -0,0 +1,174 @@ +import { environment } from '@gauzy/ui-config'; + +/** + * Retrieves the value of a cookie by its name for the current domain and its subdomains. + * + * @param {string} name - The name of the cookie to retrieve. + * @return {string | null} - The value of the cookie if found, or null if not found. + */ +export function getCookie(name: string): string | null { + if (!name || typeof name !== 'string') { + return null; + } + + // Sanitize the cookie name + const sanitizedName = encodeURIComponent(name); + const value = `; ${document.cookie}`; // Get all cookies as a string and add a leading semicolon + const parts = value.split(`; ${sanitizedName}=`); // Split the string by the desired cookie name + + // If the cookie is found, split to isolate its value and return it + if (parts.length === 2) { + const cookie = parts.pop()?.split(';').shift() || null; // Get the cookie value + + // Validate if the cookie is set for the current domain or its subdomains + if (isCookieForValidDomain(cookie)) { + return decodeURIComponent(cookie); // Return the cookie value if it's for a valid domain + } + } + + // Return null if the cookie is not found + return null; +} + +/** + * Checks if the cookie is set for the current domain, its subdomains, or localhost. + * + * @param {string} cookie - The value of the cookie to check. + * @return {boolean} - True if the cookie is considered valid, otherwise false. + */ +function isCookieForValidDomain(cookie: string | null): boolean { + // Check if the cookie is not null + if (cookie === null) { + return false; // Not valid if cookie does not exist + } + + // Get the current hostname + const hostname = window.location.hostname; // e.g., "demo.gauzy.co" or "app.gauzy.co" + + // Define allowed domains for each environment + const DOMAIN_CONFIG = { + production: ['gauzy.co', 'app.gauzy.co'], + demo: ['demo.gauzy.co'], + staging: ['staging.gauzy.co'], + development: ['localhost', '127.0.0.1'] + } as const; + + // Check for development environments + if (DOMAIN_CONFIG.development.includes(hostname as 'localhost' | '127.0.0.1')) { + return true; // Allow cookies for localhost and 127.0.0.1 + } + + // Get environment-specific domains + const validDomains = [...DOMAIN_CONFIG.production, ...DOMAIN_CONFIG.demo, ...DOMAIN_CONFIG.staging]; + + // More robust domain validation + return validDomains.some((domain: string) => { + // Convert hostname and domain to lowercase for case-insensitive comparison + const normalizedHostname = hostname.toLowerCase(); + const normalizedDomain = domain.toLowerCase(); + + // Check for exact match + if (normalizedHostname === normalizedDomain) { + return true; + } + + // Check if the hostname ends with the domain and ensure proper boundaries + if (normalizedHostname.endsWith(`.${normalizedDomain}`)) { + // Ensure there are no additional dots to prevent attacks + const subdomain = normalizedHostname.slice(0, -normalizedDomain.length - 1); + return !subdomain.includes('.'); + } + + // Prevent direct domain spoofing by checking if it matches the exact domain + if (normalizedHostname === `www.${normalizedDomain}`) { + return true; + } + + return false; // Invalid if none of the checks pass + }); +} + +/** + * Prepares cookie options with default values and overrides. + * + * @param {Object} [options={}] - Additional options to customize the cookie settings. + * @param {string} [options.path='/'] - The path where the cookie is accessible. + * @param {string} [options.SameSite='Lax'] - SameSite attribute to control cookie sharing across sites. + * @param {boolean} [options.Secure] - If true, the cookie will only be sent over HTTPS. + * @param {string} [options.domain] - The domain for which the cookie is valid. + * @returns {Object} The final cookie options object with defaults applied. + */ +function prepareCookieOptions(options: { [key: string]: any } = {}): { [key: string]: any } { + // Prepare cookie options with defaults + const cookieOptions = { + path: '/', // Default path for all cookies + SameSite: 'Lax', // Prevent CSRF attacks + Secure: window.location.protocol === 'https:', // Send only over HTTPS + ...options // Spread existing options + }; + + // Cache hostname lookup to avoid repeated access to window.location.hostname + const getCurrentHostname = (() => { + let hostname: string; + return () => (hostname ??= window.location.hostname); + })(); + + // Get current host name + const hostname = getCurrentHostname(); + if (hostname === 'localhost' || hostname === '127.0.0.1') { + cookieOptions['domain'] = undefined; // Don't set the domain for localhost + } else { + cookieOptions['domain'] = cookieOptions['domain'] || environment.COOKIE_DOMAIN; // Default domain for production + } + + return cookieOptions; // Return the final cookie options +} + +/** + * Sets a cookie with the specified name, value, and options. + * + * @param {string} name - The name of the cookie. + * @param {string} value - The value of the cookie. + * @param {Object} options - Additional options for the cookie. + */ +export function setCookie(name: string, value: string, options: { [key: string]: any } = {}) { + if (!name || typeof value === 'undefined') { + return; // Ensure valid inputs + } + + // Prepare cookie options with defaults + const cookieOptions = prepareCookieOptions(options); + + // Build the cookie string + const cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; `; + Object.entries(cookieOptions) + .map(([key, val]) => `${key}=${val}`) + .join('; '); + + // Set the cookie + document.cookie = cookieString; +} + +/** + * Deletes a cookie by setting its expiration date to a time in the past. + * + * @param {string} name - The name of the cookie to delete. + * @param {Object} options - Additional options for the cookie. + */ +export function deleteCookie(name: string, options: { [key: string]: any } = {}) { + if (!name) { + return; // Invalid name, exit function + } + + // Prepare cookie options with defaults + const cookieOptions = prepareCookieOptions(options); + + // Build the cookie string for deletion + const cookieString = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; `; + Object.entries(cookieOptions) + .map(([key, val]) => `${key}=${val}`) + .join('; '); + + // Set the cookie to delete it + document.cookie = cookieString; +} diff --git a/packages/ui-core/core/src/lib/auth/no-auth.guard.ts b/packages/ui-core/core/src/lib/auth/no-auth.guard.ts index e95f4a41b77..33f079f28aa 100644 --- a/packages/ui-core/core/src/lib/auth/no-auth.guard.ts +++ b/packages/ui-core/core/src/lib/auth/no-auth.guard.ts @@ -1,15 +1,17 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { ROUTES } from '@gauzy/ui-core/common'; import { AuthService, Store } from '../services'; + /** * Use for routes which only need to be displayed if user is NOT logged in */ @Injectable() -export class NoAuthGuard implements CanActivate { +export class NoAuthGuard { constructor( - private readonly router: Router, - private readonly authService: AuthService, - private readonly store: Store + private readonly _router: Router, + private readonly _authService: AuthService, + private readonly _store: Store ) {} /** @@ -20,18 +22,18 @@ export class NoAuthGuard implements CanActivate { * @return {Promise} A promise that resolves to true if the user is authenticated, false otherwise. */ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - if (!this.store.token) { + if (!this._store.token) { // not logged in so return true return true; } - if (!(await this.authService.isAuthenticated())) { + if (!(await this._authService.isAuthenticated())) { // not logged in so return true return true; } // logged in so redirect to dashboard - this.router.navigate(['/pages/dashboard']); + this._router.navigate([ROUTES.DASHBOARD]); return false; } diff --git a/packages/ui-core/core/src/lib/extension/index.ts b/packages/ui-core/core/src/lib/extension/index.ts new file mode 100644 index 00000000000..91ade2b9f0a --- /dev/null +++ b/packages/ui-core/core/src/lib/extension/index.ts @@ -0,0 +1 @@ +export * from './add-nav-menu-item'; diff --git a/packages/ui-core/core/src/lib/services/auth/auth-strategy.service.ts b/packages/ui-core/core/src/lib/services/auth/auth-strategy.service.ts index 3216f0954de..026313b20d5 100644 --- a/packages/ui-core/core/src/lib/services/auth/auth-strategy.service.ts +++ b/packages/ui-core/core/src/lib/services/auth/auth-strategy.service.ts @@ -12,6 +12,7 @@ import { AuthService } from './auth.service'; import { TimesheetFilterService } from '../timesheet/timesheet-filter.service'; import { TimeTrackerService } from '../time-tracker/time-tracker.service'; import { Store } from '../store/store.service'; +import { deleteCookie } from '../../auth/cookie-helper'; @Injectable() export class AuthStrategy extends NbAuthStrategy { @@ -289,6 +290,11 @@ export class AuthStrategy extends NbAuthStrategy { this.timeTrackerService.clearTimeTracker(); this.timesheetFilterService.clear(); } + + // Delete cookies + deleteCookie('userId', { SameSite: 'None', Secure: true }); // Default path + deleteCookie('token', { SameSite: 'None', Secure: true }); // Default path + deleteCookie('refresh_token', { SameSite: 'None', Secure: true }); // Default path } public login(loginInput: IUserLoginInput): Observable { diff --git a/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts b/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts index fb97a0df9f9..d1b2bd3ceb1 100644 --- a/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts +++ b/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts @@ -229,9 +229,7 @@ export class NavMenuBuilderService { } } } else { - console.error( - `Could not add menu item "${item.config.id}", section "${item.sectionId}" does not exist` - ); + this.logMenuWarning(item.config.id, item.sectionId); } }); @@ -239,4 +237,17 @@ export class NavMenuBuilderService { }) ); } + + /** + * Logs a warning message about an inability to add a menu item. + * + * @param itemId - The ID of the menu item that could not be added. + * @param sectionId - The ID of the section where the item was to be added. + * @param level - Optional logging level; defaults to 'warn'. + */ + private logMenuWarning(itemId: string, sectionId: string, level: 'warn' | 'info' | 'error' = 'warn') { + const message = `Unable to add menu item "${itemId}". Section "${sectionId}" does not exist. Please ensure the section is defined before adding items.`; + const logFn = console[level]; + logFn(message); + } }