Draft Mode inkl. Token implementiert

This commit is contained in:
Andreas Knuth 2024-05-28 11:30:00 -05:00
parent 226d2ebc1e
commit b4cf17b8ea
15 changed files with 191 additions and 137 deletions

3
.gitignore vendored
View File

@ -48,6 +48,9 @@ public
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
.env.prod
.env.dev
.env.test
# temp directory # temp directory
.temp .temp

View File

@ -6,17 +6,15 @@
"request": "launch", "request": "launch",
"name": "Debug Nest Framework", "name": "Debug Nest Framework",
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeArgs": [ "runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"run",
"start:debug",
"--",
"--inspect-brk"
],
"autoAttachChildProcesses": true, "autoAttachChildProcesses": true,
"restart": true, "restart": true,
"sourceMaps": true, "sourceMaps": true,
"stopOnEntry": false, "stopOnEntry": false,
"console": "integratedTerminal", "console": "integratedTerminal",
"env": {
"HOST_NAME": "localhost"
}
}, },
{ {
"type": "node", "type": "node",
@ -24,9 +22,7 @@
"name": "Debug Current TS File", "name": "Debug Current TS File",
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js", "program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
"preLaunchTask": "tsc: build - tsconfig.json", "preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [ "outFiles": ["${workspaceFolder}/out/**/*.js"],
"${workspaceFolder}/out/**/*.js"
],
"sourceMaps": true, "sourceMaps": true,
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart" "internalConsoleOptions": "openOnSessionStart"
@ -35,31 +31,21 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "generateDefs", "name": "generateDefs",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js", "program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
"outFiles": [ "outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true, "sourceMaps": true,
"smartStep": true, "smartStep": true
},
}, {
{ "type": "node",
"type": "node", "request": "launch",
"request": "launch", "name": "generateTypes",
"name": "generateTypes", "skipFiles": ["<node_internals>/**"],
"skipFiles": [ "program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"<node_internals>/**" "outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
], "sourceMaps": true,
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js", "smartStep": true
"outFiles": [ }
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true,
"smartStep": true,
},
] ]
} }

View File

@ -9,10 +9,10 @@
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "HOST_NAME=localhost nest start",
"start:dev": "nest start --watch", "start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",

View File

@ -1,5 +1,8 @@
import { MiddlewareConsumer, Module } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import * as dotenv from 'dotenv';
import fs from 'fs-extra';
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston'; import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -14,13 +17,37 @@ import { ListingsModule } from './listings/listings.module.js';
import { MailModule } from './mail/mail.module.js'; import { MailModule } from './mail/mail.module.js';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js'; import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
import { SelectOptionsModule } from './select-options/select-options.module.js'; import { SelectOptionsModule } from './select-options/select-options.module.js';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from './user/user.module.js'; import { UserModule } from './user/user.module.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
function loadEnvFiles() {
// Load the .env file
dotenv.config();
console.log('Loaded .env file');
// Determine which additional env file to load
let envFilePath = '';
const host = process.env.HOST_NAME || '';
if (host.includes('localhost')) {
envFilePath = '.env.local';
} else if (host.includes('dev.bizmatch.net')) {
envFilePath = '.env.dev';
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
envFilePath = '.env.prod';
}
// Load the additional env file if it exists
if (fs.existsSync(envFilePath)) {
dotenv.config({ path: envFilePath });
console.log(`Loaded ${envFilePath} file`);
} else {
console.log(`No additional .env file found for HOST_NAME: ${host}`);
}
}
loadEnvFiles();
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
@ -42,13 +69,6 @@ const __dirname = path.dirname(__filename);
], ],
// other options // other options
}), }),
// KeycloakConnectModule.register({
// authServerUrl: 'http://auth.bizmatch.net',
// realm: 'dev',
// clientId: 'dev',
// secret: 'Yu3lETbYUphDiJxgnhhpelcJ63p2FCDM',
// // Secret key of the client taken from keycloak server
// }),
GeoModule, GeoModule,
UserModule, UserModule,
ListingsModule, ListingsModule,

View File

@ -26,6 +26,7 @@ export class ImageController {
@Delete('propertyPicture/:imagePath/:serial/:imagename') @Delete('propertyPicture/:imagePath/:serial/:imagename')
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> { async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`); this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
await this.listingService.deleteImage(imagePath, serial, imagename);
} }
// ############ // ############
// Profile // Profile

View File

@ -1,11 +1,14 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa'; import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtUser } from './models/main.model';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() { constructor(configService: ConfigService) {
const realm = configService.get<string>('REALM');
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
@ -13,15 +16,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
cache: true, cache: true,
rateLimit: true, rateLimit: true,
jwksRequestsPerMinute: 5, jwksRequestsPerMinute: 5,
jwksUri: 'https://auth.bizmatch.net/realms/dev/protocol/openid-connect/certs', jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
}), }),
audience: 'account', // Keycloak Client ID audience: 'account', // Keycloak Client ID
issuer: 'https://auth.bizmatch.net/realms/dev', authorize: '',
issuer: `https://auth.bizmatch.net/realms/${realm}`,
algorithms: ['RS256'], algorithms: ['RS256'],
}); });
} }
async validate(payload: any) { async validate(payload: any): Promise<JwtUser> {
console.log('JWT Payload:', payload); // Debugging: JWT Payload anzeigen console.log('JWT Payload:', payload); // Debugging: JWT Payload anzeigen
if (!payload) { if (!payload) {
console.error('Invalid payload'); console.error('Invalid payload');

View File

@ -3,7 +3,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { businesses } from '../drizzle/schema.js'; import { businesses } from '../drizzle/schema.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { ListingCriteria } from '../models/main.model.js'; import { JwtUser, ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/business') @Controller('listings/business')
@ -13,15 +13,16 @@ export class BusinessListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.findById(id, businesses); return this.listingsService.findBusinessesById(id, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid') @Get('user/:userid')
findByUserId(@Request() req, @Param('userid') userid: string): any { findByUserId(@Request() req, @Param('userid') userid: string): any {
return this.listingsService.findByUserId(userid, businesses, req.user?.username); return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
} }
@Post('search') @Post('search')

View File

@ -1,10 +1,11 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { commercials } from '../drizzle/schema.js'; import { commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { ListingCriteria } from '../models/main.model.js'; import { JwtUser, ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
@ -15,16 +16,16 @@ export class CommercialPropertyListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.findById(id, commercials); return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid') @Get('user/:email')
findByUserId(@Request() req, @Param('userid') userid: string): any { findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
console.log(req.user?.username); return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
return this.listingsService.findByUserId(userid, commercials, req.user?.username);
} }
@Post('search') @Post('search')
async find(@Body() criteria: ListingCriteria): Promise<any> { async find(@Body() criteria: ListingCriteria): Promise<any> {

View File

@ -7,7 +7,7 @@ import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js'; import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { ListingCriteria } from '../models/main.model.js'; import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable() @Injectable()
export class ListingsService { export class ListingsService {
@ -64,12 +64,21 @@ export class ListingsService {
]); ]);
return { total, data }; return { total, data };
} }
async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> { async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
const result = await this.conn let result = await this.conn
.select() .select()
.from(table) .from(commercials)
.where(and(sql`${table.id} = ${id}`, ne(table.draft, true))); .where(and(sql`${commercials.id} = ${id}`));
return result[0] as BusinessListing | CommercialPropertyListing; result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user.username) || user.roles.includes('ADMIN'));
return result[0] as CommercialPropertyListing;
}
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(businesses)
.where(and(sql`${businesses.id} = ${id}`));
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user.username) || user.roles.includes('ADMIN'));
return result[0] as BusinessListing;
} }
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> { async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn const result = await this.conn
@ -78,10 +87,28 @@ export class ListingsService {
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`, ne(commercials.draft, true))); .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`, ne(commercials.draft, true)));
return result[0] as CommercialPropertyListing; return result[0] as CommercialPropertyListing;
} }
async findByUserId(userId: string, table: typeof businesses | typeof commercials, email: string): Promise<BusinessListing[] | CommercialPropertyListing[]> { async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[]; const conditions = [];
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
if (email !== user.username && !user.roles.includes('ADMIN')) {
conditions.push(ne(commercials.draft, true));
}
return (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
}
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = [];
conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user.username && !user.roles.includes('ADMIN')) {
conditions.push(ne(businesses.draft, true));
}
return (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
} }
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> { async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.created = new Date(); data.created = new Date();
data.updated = new Date(); data.updated = new Date();
@ -120,8 +147,8 @@ export class ListingsService {
// ############################################################## // ##############################################################
// Images for commercial Properties // Images for commercial Properties
// ############################################################## // ##############################################################
async deleteImage(id: string, name: string) { async deleteImage(imagePath: string, serial: string, name: string) {
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing; const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name); const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) { if (index > -1) {
listing.imageOrder.splice(index, 1); listing.imageOrder.splice(index, 1);

View File

@ -1,7 +1,6 @@
import { Controller, Get, Inject, Param } from '@nestjs/common'; import { Controller, Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { businesses, commercials } from '../drizzle/schema.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/undefined') @Controller('listings/undefined')
@ -11,13 +10,13 @@ export class UnknownListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@Get(':id') // @Get(':id')
async findById(@Param('id') id: string): Promise<any> { // async findById(@Param('id') id: string): Promise<any> {
const result = await this.listingsService.findById(id, businesses); // const result = await this.listingsService.findById(id, businesses);
if (result) { // if (result) {
return result; // return result;
} else { // } else {
return await this.listingsService.findById(id, commercials); // return await this.listingsService.findById(id, commercials);
} // }
} // }
} }

View File

@ -83,6 +83,11 @@ export interface KeycloakUser {
notBefore?: number; notBefore?: number;
access?: Access; access?: Access;
} }
export interface JwtUser {
userId: string;
username: string;
roles: string[];
}
export interface Access { export interface Access {
manageGroupMembership: boolean; manageGroupMembership: boolean;
view: boolean; view: boolean;

View File

@ -53,8 +53,7 @@ export class DetailsUserComponent {
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
this.user.email; const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse // Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
this.businessListings = results[0]; this.businessListings = results[0];
this.commercialPropListings = results[1]; this.commercialPropListings = results[1];

View File

@ -82,58 +82,66 @@
</div> </div>
</div> </div>
</div> </div>
<p-divider></p-divider> <div class="grid">
<div class="flex gap-5 flex-column-reverse md:flex-row"> <div class="mb-4 col-12 md:col-6">
<div class="flex-auto p-fluid"> <p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<div class="grid"> <span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
<div class="mb-4 col-12 md:col-6"> </div>
<label for="price" class="block font-medium text-900 mb-2">Price</label> </div>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> --> <div>
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber> <p-divider></p-divider>
</div> <div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="mb-4 col-12 md:col-6"> <div class="flex-auto p-fluid">
<div class="flex flex-column align-items-center flex-or"> <div class="grid">
<span class="font-medium text-900 mb-2">Property Pictures</span> <div class="mb-4 col-12 md:col-6">
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span> <label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> --> <!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<p-fileUpload <app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
mode="basic" </div>
chooseLabel="Upload" <div class="mb-4 col-12 md:col-6">
[customUpload]="true" <div class="flex flex-column align-items-center flex-or">
name="file" <span class="font-medium text-900 mb-2">Property Pictures</span>
accept="image/*" <span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
[maxFileSize]="maxFileSize" <!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
(onSelect)="select($event)" <p-fileUpload
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4" mode="basic"
[disabled]="!listing.id" chooseLabel="Upload"
> [customUpload]="true"
</p-fileUpload> name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
[disabled]="!listing.id"
>
</p-fileUpload>
</div>
</div> </div>
</div> </div>
</div> @if (listing && listing.imageOrder?.length>0){
@if (listing && listing.imageOrder?.length>0){ <div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal"> @for (image of listing.imageOrder; track listing.imageOrder) {
@for (image of listing.imageOrder; track listing.imageOrder) { <span cdkDropList mixedCdkDropList>
<span cdkDropList mixedCdkDropList> <div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap"> <img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag /> <fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon> </div>
</div> </span>
</span> }
} </div>
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
} }
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>
</div> </div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

@ -36,7 +36,7 @@ export class MyListingComponent {
const keycloakUser = map2User(token); const keycloakUser = map2User(token);
const email = keycloakUser.email; const email = keycloakUser.email;
this.user = await this.userService.getByMail(email); this.user = await this.userService.getByMail(email);
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]); const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
this.myListings = [...result[0], ...result[1]]; this.myListings = [...result[0], ...result[1]];
} }
@ -46,7 +46,7 @@ export class MyListingComponent {
} else { } else {
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath); await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
} }
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]); const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
this.myListings = [...result[0], ...result[1]]; this.myListings = [...result[0], ...result[1]];
} }

View File

@ -20,8 +20,8 @@ export class ListingsService {
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`); const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
return result; return result;
} }
getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> { getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`)); return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
} }
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') { async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (listing.id) { if (listing.id) {