Ai Search finished, some makeup changes, guards, brokerSearch

This commit is contained in:
Andreas Knuth 2024-08-28 15:41:59 +02:00
parent 8721be4a90
commit ede8b66d83
20 changed files with 139 additions and 53 deletions

View File

@ -14,6 +14,5 @@ export class AppController {
@Get()
getHello(@Request() req): string {
return req.user;
//return 'dfgdf';
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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)));
}

View File

@ -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<Subscription[]> {
const subscriptions = this.fileService.getSubscriptions();

View File

@ -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(),

View File

@ -124,7 +124,7 @@ export const routes: Routes = [
{
path: 'emailUs',
component: EmailUsComponent,
canActivate: [AuthGuard],
// canActivate: [AuthGuard],
},
// #########
// Logout

View File

@ -73,7 +73,7 @@
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
</li>
<li>
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
<a routerLink="/pricing" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
</li>
</ul>
</div>

View File

@ -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;
}

View File

@ -330,21 +330,23 @@
<div>
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
<div class="flex items-center space-x-2">
<input
<!-- <input
type="number"
id="price-from"
[(ngModel)]="criteria.minPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
/> -->
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<span>-</span>
<input
<app-validated-price name="price-to" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<!-- <input
type="number"
id="price-to"
[(ngModel)]="criteria.maxPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
/> -->
</div>
</div>
<div>

View File

@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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);
}),
);
}
}

View File

@ -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)) {

View File

@ -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)) {

View File

@ -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 */

View File

@ -110,7 +110,9 @@
</button>
</div>
</div>
} @if(criteria && !aiSearch){
@if(aiSearchFailed){
<div id="error-message" class="w-full max-w-3xl mx-auto mt-2 text-red-600 text-center">Search timed out. Please try again or use classic Search</div>
} } @if(criteria && !aiSearch){
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
<div class="md:flex-none md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
<div class="relative max-sm:border border-gray-300 rounded-md">
@ -184,7 +186,8 @@
}
<div class="mt-4 flex items-center justify-center text-gray-700">
<span class="mr-2">AI-Search</span>
<span class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
<span [attr.data-tooltip-target]="tooltipTargetBeta" class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
<app-tooltip [id]="tooltipTargetBeta" text="The AI will convert your input into filter criteria. Please check them in the filter menu after the search"></app-tooltip>
<span class="ml-2">- Try now</span>
<div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input (click)="toggleAiSearch()" type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" />

View File

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

View File

@ -137,6 +137,6 @@
<div class="mt-16 text-center">
<h2 class="text-2xl font-semibold mb-4">Not sure which plan is right for you?</h2>
<p class="text-gray-600 mb-8">Contact our sales team for a personalized recommendation.</p>
<a href="#" class="bg-blue-500 text-white rounded-full px-6 py-3 font-semibold hover:bg-blue-600 transition duration-300">Contact Sales</a>
<a routerLink="/emailUs" class="bg-blue-500 text-white rounded-full px-6 py-3 font-semibold hover:bg-blue-600 transition duration-300">Contact Sales</a>
</div>
</div>

View File

@ -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)) {

View File

@ -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)) {

View File

@ -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);