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.production.local
.env.local
.env.prod
.env.dev
.env.test
# temp directory
.temp

View File

@ -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": [
"<node_internals>/**"
],
"skipFiles": ["<node_internals>/**"],
"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": [
"<node_internals>/**"
],
"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": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"sourceMaps": true,
"smartStep": true
}
]
}

View File

@ -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",

View File

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

View File

@ -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<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
await this.listingService.deleteImage(imagePath, serial, imagename);
}
// ############
// Profile

View File

@ -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<string>('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<JwtUser> {
console.log('JWT Payload:', payload); // Debugging: JWT Payload anzeigen
if (!payload) {
console.error('Invalid payload');

View File

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

View File

@ -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<CommercialPropertyListing[]> {
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
}
@Post('search')
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 { 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<BusinessListing | CommercialPropertyListing> {
const result = await this.conn
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
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<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> {
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<BusinessListing[] | CommercialPropertyListing[]> {
return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[];
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<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> {
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);

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 { 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<any> {
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<any> {
// const result = await this.listingsService.findById(id, businesses);
// if (result) {
// return result;
// } else {
// return await this.listingsService.findById(id, commercials);
// }
// }
}

View File

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

View File

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

View File

@ -82,58 +82,66 @@
</div>
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
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 class="grid">
<div class="mb-4 col-12 md:col-6">
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
</div>
</div>
<div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
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>
@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">
@for (image of listing.imageOrder; track listing.imageOrder) {
<span cdkDropList mixedCdkDropList>
<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 />
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
</div>
</span>
}
</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>
@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">
@for (image of listing.imageOrder; track listing.imageOrder) {
<span cdkDropList mixedCdkDropList>
<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 />
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
</div>
</span>
}
</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>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

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

View File

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