diff --git a/bizmatch-server/src/app.controller.ts b/bizmatch-server/src/app.controller.ts index 3f0ae2c..4feaacd 100644 --- a/bizmatch-server/src/app.controller.ts +++ b/bizmatch-server/src/app.controller.ts @@ -14,6 +14,5 @@ export class AppController { @Get() getHello(@Request() req): string { return req.user; - //return 'dfgdf'; } } diff --git a/bizmatch-server/src/auth/auth.controller.ts b/bizmatch-server/src/auth/auth.controller.ts index 8932cdd..229bbf0 100644 --- a/bizmatch-server/src/auth/auth.controller.ts +++ b/bizmatch-server/src/auth/auth.controller.ts @@ -1,24 +1,30 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { AdminAuthGuard } from '../jwt-auth/admin-auth.guard.js'; import { AuthService } from './auth.service.js'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} + @UseGuards(AdminAuthGuard) @Get() getAccessToken(): any { return this.authService.getAccessToken(); } + @UseGuards(AdminAuthGuard) @Get('users') getUsers(): any { return this.authService.getUsers(); } + + @UseGuards(AdminAuthGuard) @Get('user/:userid') getUser(@Param('userid') userId: string): any { return this.authService.getUser(userId); } + @UseGuards(AdminAuthGuard) @Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth getLastLogin(@Param('userid') userId: string): any { return this.authService.getLastLogin(userId); diff --git a/bizmatch-server/src/jwt-auth/admin-auth.guard.ts b/bizmatch-server/src/jwt-auth/admin-auth.guard.ts new file mode 100644 index 0000000..045fad5 --- /dev/null +++ b/bizmatch-server/src/jwt-auth/admin-auth.guard.ts @@ -0,0 +1,18 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class AdminAuthGuard extends AuthGuard('jwt') implements CanActivate { + canActivate(context: ExecutionContext) { + // Add your custom authentication logic here + // for example, call super.logIn(request) to establish a session. + return super.canActivate(context); + } + handleRequest(err, user, info) { + // You can throw an exception based on either "info" or "err" arguments + if (err || !user || !user.roles.includes('ADMIN')) { + throw err || new UnauthorizedException(info); + } + return user; + } +} diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index 28cd72d..8f01120 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -10,7 +10,7 @@ import { FileService } from '../file/file.service.js'; import { GeoService } from '../geo/geo.service.js'; import { BusinessListing, BusinessListingSchema } from '../models/db.model.js'; import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js'; -import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.js'; +import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery, splitName } from '../utils.js'; @Injectable() export class BusinessListingService { @@ -94,10 +94,17 @@ export class BusinessListingService { if (criteria.title) { whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`))); } - if (criteria.brokerName) { - whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`))); + const { firstname, lastname } = splitName(criteria.brokerName); + if (firstname === lastname) { + whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`))); + } else { + whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`))); + } } + // if (criteria.brokerName) { + // whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`))); + // } if (!user?.roles?.includes('ADMIN') ?? false) { whereConditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true))); } diff --git a/bizmatch-server/src/user/user.controller.ts b/bizmatch-server/src/user/user.controller.ts index f8ce70e..0300c59 100644 --- a/bizmatch-server/src/user/user.controller.ts +++ b/bizmatch-server/src/user/user.controller.ts @@ -3,6 +3,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { ZodError } from 'zod'; import { FileService } from '../file/file.service.js'; +import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { User } from '../models/db.model'; import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js'; @@ -77,6 +78,7 @@ export class UserController { return result; } + @UseGuards(JwtAuthGuard) @Get('subscriptions/:id') async findSubscriptionsById(@Param('id') id: string): Promise { const subscriptions = this.fileService.getSubscriptions(); diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index 2c092bb..fb5c86f 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -10,6 +10,7 @@ import { environment } from '../environments/environment'; import { customKeycloakAdapter } from '../keycloak'; import { routes } from './app.routes'; import { LoadingInterceptor } from './interceptors/loading.interceptor'; +import { TimeoutInterceptor } from './interceptors/timeout.interceptor'; import { KeycloakInitializerService } from './services/keycloak-initializer.service'; import { SelectOptionsService } from './services/select-options.service'; import { createLogger } from './utils/utils'; @@ -44,6 +45,15 @@ export const appConfig: ApplicationConfig = { useClass: KeycloakBearerInterceptor, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: TimeoutInterceptor, + multi: true, + }, + { + provide: 'TIMEOUT_DURATION', + useValue: 5000, // Standard-Timeout von 5 Sekunden + }, provideRouter( routes, withEnabledBlockingInitialNavigation(), diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index f03102c..9398c62 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -124,7 +124,7 @@ export const routes: Routes = [ { path: 'emailUs', component: EmailUsComponent, - canActivate: [AuthGuard], + // canActivate: [AuthGuard], }, // ######### // Logout diff --git a/bizmatch/src/app/components/header/header.component.html b/bizmatch/src/app/components/header/header.component.html index c5c82dc..487d99f 100644 --- a/bizmatch/src/app/components/header/header.component.html +++ b/bizmatch/src/app/components/header/header.component.html @@ -73,7 +73,7 @@ Log In
  • - Register + Register
  • diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index 39cd1a7..9aa855b 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -109,9 +109,7 @@ export class HeaderComponent { redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`, }); } - register() { - this.keycloakService.register({ redirectUri: `${window.location.origin}/account` }); - } + isActive(route: string): boolean { return this.router.url === route; } diff --git a/bizmatch/src/app/components/search-modal/search-modal.component.html b/bizmatch/src/app/components/search-modal/search-modal.component.html index fce32db..33ecb04 100644 --- a/bizmatch/src/app/components/search-modal/search-modal.component.html +++ b/bizmatch/src/app/components/search-modal/search-modal.component.html @@ -330,21 +330,23 @@
    - + /> --> + - - +
    diff --git a/bizmatch/src/app/interceptors/timeout.interceptor.ts b/bizmatch/src/app/interceptors/timeout.interceptor.ts new file mode 100644 index 0000000..ce0c3e5 --- /dev/null +++ b/bizmatch/src/app/interceptors/timeout.interceptor.ts @@ -0,0 +1,28 @@ +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; + +@Injectable() +export class TimeoutInterceptor implements HttpInterceptor { + constructor(@Optional() @Inject('TIMEOUT_DURATION') private timeoutDuration: number = 5000) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + timeout(this.timeoutDuration), + catchError((error: any) => { + if (error instanceof TimeoutError) { + // Timeout error handling + return throwError( + () => + new HttpErrorResponse({ + error: 'Request timed out', + status: 408, // HTTP status code for Request Timeout + }), + ); + } + return throwError(() => error); + }), + ); + } +} diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts index 7827833..4465aea 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts @@ -110,7 +110,7 @@ export class DetailsBusinessListingComponent { } catch (error) { this.messageService.addMessage({ severity: 'danger', - text: 'An error occurred while sending the request', + text: 'An error occurred while sending the request - Please check your inputs', duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts index dd70cca..2bf5989 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts @@ -138,7 +138,7 @@ export class DetailsCommercialPropertyListingComponent { } catch (error) { this.messageService.addMessage({ severity: 'danger', - text: 'An error occurred while sending the request', + text: 'An error occurred while sending the request - Please check your inputs', duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { diff --git a/bizmatch/src/app/pages/details/details.scss b/bizmatch/src/app/pages/details/details.scss index b0dba7a..fa8d1d3 100644 --- a/bizmatch/src/app/pages/details/details.scss +++ b/bizmatch/src/app/pages/details/details.scss @@ -1,7 +1,7 @@ :host ::ng-deep p { display: block; - margin-top: 1em; - margin-bottom: 1em; + // margin-top: 1em; + // margin-bottom: 1em; margin-left: 0; margin-right: 0; font-size: 1rem; /* oder 1rem, abhängig vom Browser und den Standardeinstellungen */ diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index acb5b82..2807e5a 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -110,7 +110,9 @@
    - } @if(criteria && !aiSearch){ + @if(aiSearchFailed){ +
    Search timed out. Please try again or use classic Search
    + } } @if(criteria && !aiSearch){
    @@ -184,7 +186,8 @@ }
    AI-Search - BETA + BETA + - Try now
    diff --git a/bizmatch/src/app/pages/home/home.component.ts b/bizmatch/src/app/pages/home/home.component.ts index 3732330..a4a5aaa 100644 --- a/bizmatch/src/app/pages/home/home.component.ts +++ b/bizmatch/src/app/pages/home/home.component.ts @@ -4,10 +4,12 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { initFlowbite } from 'flowbite'; import { KeycloakService } from 'keycloak-angular'; import { catchError, concat, debounceTime, distinctUntilChanged, lastValueFrom, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs'; import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { ModalService } from '../../components/search-modal/modal.service'; +import { TooltipComponent } from '../../components/tooltip/tooltip.component'; import { AiService } from '../../services/ai.service'; import { CriteriaChangeService } from '../../services/criteria-change.service'; import { GeoService } from '../../services/geo.service'; @@ -29,12 +31,12 @@ import { @Component({ selector: 'app-home', standalone: true, - imports: [CommonModule, FormsModule, RouterModule, NgSelectModule], + imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, TooltipComponent], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) export class HomeComponent { - placeholders: string[] = ['waterfront property close to Houston less than 1M', 'construction area with beach access close to San Diego']; + placeholders: string[] = ['Property close to Houston less than 10M', 'Franchise business in Austin price less than 500K']; activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business'; type: string; maxPrice: string; @@ -53,6 +55,7 @@ export class HomeComponent { aiSearch = false; aiSearchText = ''; + aiSearchFailed = false; loadingAi = false; @ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef; typingSpeed: number = 100; // Geschwindigkeit des Tippens (ms) @@ -61,7 +64,7 @@ export class HomeComponent { charIndex: number = 0; typingInterval: any; showInput: boolean = true; // Steuerung der Anzeige des Eingabefelds - + tooltipTargetBeta = 'tooltipTargetBeta'; public constructor( private router: Router, private modalService: ModalService, @@ -77,6 +80,9 @@ export class HomeComponent { private aiService: AiService, ) {} async ngOnInit() { + setTimeout(() => { + initFlowbite(); + }, 0); const token = await this.keycloakService.getToken(); sessionStorage.removeItem('businessListings'); sessionStorage.removeItem('commercialPropertyListings'); @@ -219,6 +225,7 @@ export class HomeComponent { } toggleAiSearch() { this.aiSearch = !this.aiSearch; + this.aiSearchFailed = false; if (!this.aiSearch) { this.aiSearchText = ''; this.stopTypingEffect(); @@ -271,33 +278,39 @@ export class HomeComponent { } async generateAiResponse() { this.loadingAi = true; - const result = await this.aiService.generateAiReponse(this.aiSearchText); - console.log(result); - let criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria | any; - if (result.criteriaType === 'businessListings') { - this.changeTab('business'); - criteria = result as BusinessListingCriteria; - } else if (result.criteriaType === 'commercialPropertyListings') { - this.changeTab('commercialProperty'); - criteria = result as CommercialPropertyListingCriteria; - } else { - this.changeTab('broker'); - criteria = result as UserListingCriteria; - } - const city = criteria.city as string; - if (city && city.length > 0) { - let results = await lastValueFrom(this.geoService.findCitiesStartingWith(city, criteria.state)); - if (results.length > 0) { - criteria.city = results[0]; + this.aiSearchFailed = false; + try { + const result = await this.aiService.generateAiReponse(this.aiSearchText); + let criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria | any; + if (result.criteriaType === 'businessListings') { + this.changeTab('business'); + criteria = result as BusinessListingCriteria; + } else if (result.criteriaType === 'commercialPropertyListings') { + this.changeTab('commercialProperty'); + criteria = result as CommercialPropertyListingCriteria; } else { - criteria.city = null; + this.changeTab('broker'); + criteria = result as UserListingCriteria; } + const city = criteria.city as string; + if (city && city.length > 0) { + let results = await lastValueFrom(this.geoService.findCitiesStartingWith(city, criteria.state)); + if (results.length > 0) { + criteria.city = results[0]; + } else { + criteria.city = null; + } + } + if (criteria.radius && criteria.radius.length > 0) { + criteria.radius = parseInt(criteria.radius); + } + this.loadingAi = false; + this.criteria = assignProperties(this.criteria, criteria); + this.search(); + } catch (error) { + console.log(error); + this.aiSearchFailed = true; + this.loadingAi = false; } - if (criteria.radius && criteria.radius.length > 0) { - criteria.radius = parseInt(criteria.radius); - } - this.loadingAi = false; - this.criteria = assignProperties(this.criteria, criteria); - this.search(); } } diff --git a/bizmatch/src/app/pages/pricing/pricing.component.html b/bizmatch/src/app/pages/pricing/pricing.component.html index 99bdbea..2ab7f6d 100644 --- a/bizmatch/src/app/pages/pricing/pricing.component.html +++ b/bizmatch/src/app/pages/pricing/pricing.component.html @@ -137,6 +137,6 @@

    Not sure which plan is right for you?

    Contact our sales team for a personalized recommendation.

    - Contact Sales + Contact Sales
    diff --git a/bizmatch/src/app/pages/subscription/account/account.component.ts b/bizmatch/src/app/pages/subscription/account/account.component.ts index cb5be76..1b0b564 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.ts +++ b/bizmatch/src/app/pages/subscription/account/account.component.ts @@ -173,7 +173,7 @@ export class AccountComponent { } catch (error) { this.messageService.addMessage({ severity: 'danger', - text: 'An error occurred while saving the profile', + text: 'An error occurred while saving the profile - Please check your inputs', duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { diff --git a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts index 6ce8f5f..7c901ab 100644 --- a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts @@ -128,7 +128,7 @@ export class EditBusinessListingComponent { } catch (error) { this.messageService.addMessage({ severity: 'danger', - text: 'An error occurred while saving the profile', + text: 'An error occurred while saving the profile - Please check your inputs', duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { diff --git a/bizmatch/src/app/pages/subscription/email-us/email-us.component.ts b/bizmatch/src/app/pages/subscription/email-us/email-us.component.ts index 118a957..b0b9eed 100644 --- a/bizmatch/src/app/pages/subscription/email-us/email-us.component.ts +++ b/bizmatch/src/app/pages/subscription/email-us/email-us.component.ts @@ -57,8 +57,8 @@ export class EmailUsComponent { } catch (error) { this.messageService.addMessage({ severity: 'danger', - text: 'An error occurred', - duration: 50000, + text: 'Please check your inputs', + duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { this.validationMessagesService.updateMessages(error.error.message);