diff --git a/.gitignore b/.gitignore index 3f95515..fef6a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ public .env.test.local .env.production.local .env.local +.env.prod +.env.dev +.env.test # temp directory .temp diff --git a/bizmatch-server/.vscode/launch.json b/bizmatch-server/.vscode/launch.json index b46eb34..c094940 100644 --- a/bizmatch-server/.vscode/launch.json +++ b/bizmatch-server/.vscode/launch.json @@ -6,17 +6,15 @@ "request": "launch", "name": "Debug Nest Framework", "runtimeExecutable": "npm", - "runtimeArgs": [ - "run", - "start:debug", - "--", - "--inspect-brk" - ], + "runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"], "autoAttachChildProcesses": true, "restart": true, "sourceMaps": true, "stopOnEntry": false, "console": "integratedTerminal", + "env": { + "HOST_NAME": "localhost" + } }, { "type": "node", @@ -24,9 +22,7 @@ "name": "Debug Current TS File", "program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js", "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], "sourceMaps": true, "smartStep": true, "internalConsoleOptions": "openOnSessionStart" @@ -35,31 +31,21 @@ "type": "node", "request": "launch", "name": "generateDefs", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js", - "outFiles": [ - "${workspaceFolder}/dist/src/drizzle/**/*.js" - ], + "outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"], "sourceMaps": true, - "smartStep": true, - - }, - { - "type": "node", - "request": "launch", - "name": "generateTypes", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js", - "outFiles": [ - "${workspaceFolder}/dist/src/drizzle/**/*.js" - ], - "sourceMaps": true, - "smartStep": true, - -}, + "smartStep": true + }, + { + "type": "node", + "request": "launch", + "name": "generateTypes", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js", + "outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"], + "sourceMaps": true, + "smartStep": true + } ] -} \ No newline at end of file +} diff --git a/bizmatch-server/package.json b/bizmatch-server/package.json index 4847626..aa58df4 100644 --- a/bizmatch-server/package.json +++ b/bizmatch-server/package.json @@ -9,10 +9,10 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", + "start": "HOST_NAME=localhost nest start", + "start:dev": "HOST_NAME=dev.bizmatch.net nest start --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", "test": "jest", "test:watch": "jest --watch", @@ -111,4 +111,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/bizmatch-server/src/app.module.ts b/bizmatch-server/src/app.module.ts index 1c67b6e..6cb91c2 100644 --- a/bizmatch-server/src/app.module.ts +++ b/bizmatch-server/src/app.module.ts @@ -1,5 +1,8 @@ import { MiddlewareConsumer, Module } from '@nestjs/common'; 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 path from 'path'; import { fileURLToPath } from 'url'; @@ -14,13 +17,37 @@ import { ListingsModule } from './listings/listings.module.js'; import { MailModule } from './mail/mail.module.js'; import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js'; import { SelectOptionsModule } from './select-options/select-options.module.js'; - -import { PassportModule } from '@nestjs/passport'; import { UserModule } from './user/user.module.js'; - const __filename = fileURLToPath(import.meta.url); 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({ imports: [ ConfigModule.forRoot({ isGlobal: true }), @@ -42,13 +69,6 @@ const __dirname = path.dirname(__filename); ], // 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, UserModule, ListingsModule, diff --git a/bizmatch-server/src/image/image.controller.ts b/bizmatch-server/src/image/image.controller.ts index 0046769..6e30e17 100644 --- a/bizmatch-server/src/image/image.controller.ts +++ b/bizmatch-server/src/image/image.controller.ts @@ -26,6 +26,7 @@ export class ImageController { @Delete('propertyPicture/:imagePath/:serial/:imagename') async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise { this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`); + await this.listingService.deleteImage(imagePath, serial, imagename); } // ############ // Profile diff --git a/bizmatch-server/src/jwt.strategy.ts b/bizmatch-server/src/jwt.strategy.ts index ba64d28..6115260 100644 --- a/bizmatch-server/src/jwt.strategy.ts +++ b/bizmatch-server/src/jwt.strategy.ts @@ -1,11 +1,14 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtUser } from './models/main.model'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { + constructor(configService: ConfigService) { + const realm = configService.get('REALM'); super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, @@ -13,15 +16,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) { cache: true, rateLimit: true, 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 - issuer: 'https://auth.bizmatch.net/realms/dev', + authorize: '', + issuer: `https://auth.bizmatch.net/realms/${realm}`, algorithms: ['RS256'], }); } - async validate(payload: any) { + async validate(payload: any): Promise { console.log('JWT Payload:', payload); // Debugging: JWT Payload anzeigen if (!payload) { console.error('Invalid payload'); diff --git a/bizmatch-server/src/listings/business-listings.controller.ts b/bizmatch-server/src/listings/business-listings.controller.ts index 578cf9b..aa50f4f 100644 --- a/bizmatch-server/src/listings/business-listings.controller.ts +++ b/bizmatch-server/src/listings/business-listings.controller.ts @@ -3,7 +3,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { businesses } from '../drizzle/schema.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'; @Controller('listings/business') @@ -13,15 +13,16 @@ export class BusinessListingsController { @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, ) {} + @UseGuards(OptionalJwtAuthGuard) @Get(':id') - findById(@Param('id') id: string): any { - return this.listingsService.findById(id, businesses); + findById(@Request() req, @Param('id') id: string): any { + return this.listingsService.findBusinessesById(id, req.user as JwtUser); } @UseGuards(OptionalJwtAuthGuard) @Get('user/:userid') 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') diff --git a/bizmatch-server/src/listings/commercial-property-listings.controller.ts b/bizmatch-server/src/listings/commercial-property-listings.controller.ts index 48203c5..c1fa585 100644 --- a/bizmatch-server/src/listings/commercial-property-listings.controller.ts +++ b/bizmatch-server/src/listings/commercial-property-listings.controller.ts @@ -1,10 +1,11 @@ import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { CommercialPropertyListing } from 'src/models/db.model.js'; import { Logger } from 'winston'; import { commercials } from '../drizzle/schema.js'; import { FileService } from '../file/file.service.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'; @Controller('listings/commercialProperty') @@ -15,16 +16,16 @@ export class CommercialPropertyListingsController { @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, ) {} + @UseGuards(OptionalJwtAuthGuard) @Get(':id') - findById(@Param('id') id: string): any { - return this.listingsService.findById(id, commercials); + findById(@Request() req, @Param('id') id: string): any { + return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser); } @UseGuards(OptionalJwtAuthGuard) - @Get('user/:userid') - findByUserId(@Request() req, @Param('userid') userid: string): any { - console.log(req.user?.username); - return this.listingsService.findByUserId(userid, commercials, req.user?.username); + @Get('user/:email') + findByEmail(@Request() req, @Param('email') email: string): Promise { + return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser); } @Post('search') async find(@Body() criteria: ListingCriteria): Promise { diff --git a/bizmatch-server/src/listings/listings.service.ts b/bizmatch-server/src/listings/listings.service.ts index bfcfb00..909912a 100644 --- a/bizmatch-server/src/listings/listings.service.ts +++ b/bizmatch-server/src/listings/listings.service.ts @@ -7,7 +7,7 @@ import { Logger } from 'winston'; import * as schema from '../drizzle/schema.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.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() export class ListingsService { @@ -64,12 +64,21 @@ export class ListingsService { ]); return { total, data }; } - async findById(id: string, table: typeof businesses | typeof commercials): Promise { - const result = await this.conn + async findCommercialPropertiesById(id: string, user: JwtUser): Promise { + let result = await this.conn .select() - .from(table) - .where(and(sql`${table.id} = ${id}`, ne(table.draft, true))); - return result[0] as BusinessListing | CommercialPropertyListing; + .from(commercials) + .where(and(sql`${commercials.id} = ${id}`)); + 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 { + 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 { 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))); return result[0] as CommercialPropertyListing; } - async findByUserId(userId: string, table: typeof businesses | typeof commercials, email: string): Promise { - return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[]; + async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise { + 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 { + 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 { data.created = new Date(); data.updated = new Date(); @@ -120,8 +147,8 @@ export class ListingsService { // ############################################################## // Images for commercial Properties // ############################################################## - async deleteImage(id: string, name: string) { - const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing; + async deleteImage(imagePath: string, serial: string, name: string) { + const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing; const index = listing.imageOrder.findIndex(im => im === name); if (index > -1) { listing.imageOrder.splice(index, 1); diff --git a/bizmatch-server/src/listings/unknown-listings.controller.ts b/bizmatch-server/src/listings/unknown-listings.controller.ts index 9bd7d77..8e197ad 100644 --- a/bizmatch-server/src/listings/unknown-listings.controller.ts +++ b/bizmatch-server/src/listings/unknown-listings.controller.ts @@ -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 { Logger } from 'winston'; -import { businesses, commercials } from '../drizzle/schema.js'; import { ListingsService } from './listings.service.js'; @Controller('listings/undefined') @@ -11,13 +10,13 @@ export class UnknownListingsController { @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, ) {} - @Get(':id') - async findById(@Param('id') id: string): Promise { - const result = await this.listingsService.findById(id, businesses); - if (result) { - return result; - } else { - return await this.listingsService.findById(id, commercials); - } - } + // @Get(':id') + // async findById(@Param('id') id: string): Promise { + // const result = await this.listingsService.findById(id, businesses); + // if (result) { + // return result; + // } else { + // return await this.listingsService.findById(id, commercials); + // } + // } } diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index 5926c19..f538650 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -83,6 +83,11 @@ export interface KeycloakUser { notBefore?: number; access?: Access; } +export interface JwtUser { + userId: string; + username: string; + roles: string[]; +} export interface Access { manageGroupMembership: boolean; view: boolean; diff --git a/bizmatch/src/app/pages/details/details-user/details-user.component.ts b/bizmatch/src/app/pages/details/details-user/details-user.component.ts index 6f2d69b..6d9a0e1 100644 --- a/bizmatch/src/app/pages/details/details-user/details-user.component.ts +++ b/bizmatch/src/app/pages/details/details-user/details-user.component.ts @@ -53,8 +53,7 @@ export class DetailsUserComponent { async ngOnInit() { this.user = await this.userService.getById(this.id); - this.user.email; - const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]); + const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]); // Zuweisen der Ergebnisse zu den Member-Variablen der Klasse this.businessListings = results[0]; this.commercialPropListings = results[1]; diff --git a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html index dd97b24..1cbde4c 100644 --- a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html +++ b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html @@ -82,58 +82,66 @@ - -
-
-
-
- - - -
-
-
- Property Pictures - (Pictures can be uploaded once the listing is posted initially) - - - +
+
+ + Draft Mode (Will not be shown as public listing) +
+
+
+ +
+
+
+
+ + + +
+
+
+ Property Pictures + (Pictures can be uploaded once the listing is posted initially) + + + +
-
- @if (listing && listing.imageOrder?.length>0){ -
- @for (image of listing.imageOrder; track listing.imageOrder) { - -
- - -
-
- } -
- } -
- @if (mode==='create'){ - - } @else { - + @if (listing && listing.imageOrder?.length>0){ +
+ @for (image of listing.imageOrder; track listing.imageOrder) { + +
+ + +
+
+ } +
} +
+ @if (mode==='create'){ + + } @else { + + } +
+ +
- - diff --git a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts index f41403a..ca705b1 100644 --- a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.ts @@ -36,7 +36,7 @@ export class MyListingComponent { const keycloakUser = map2User(token); const email = keycloakUser.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]]; } @@ -46,7 +46,7 @@ export class MyListingComponent { } else { await this.listingsService.deleteCommercialPropertyListing(listing.id, (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]]; } diff --git a/bizmatch/src/app/services/listings.service.ts b/bizmatch/src/app/services/listings.service.ts index 49a6fcd..87a36b0 100644 --- a/bizmatch/src/app/services/listings.service.ts +++ b/bizmatch/src/app/services/listings.service.ts @@ -20,8 +20,8 @@ export class ListingsService { const result = this.http.get(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`); return result; } - getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise { - return lastValueFrom(this.http.get(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`)); + getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise { + return lastValueFrom(this.http.get(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`)); } async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') { if (listing.id) {