diff --git a/bizmatch-server/src/auth/auth.controller.ts b/bizmatch-server/src/auth/auth.controller.ts index 73f3ca6..50d0d84 100644 --- a/bizmatch-server/src/auth/auth.controller.ts +++ b/bizmatch-server/src/auth/auth.controller.ts @@ -1,4 +1,6 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Put, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from 'src/jwt-auth/jwt-auth.guard'; +import { KeycloakUser } from 'src/models/main.model'; import { AdminAuthGuard } from '../jwt-auth/admin-auth.guard'; import { AuthService } from './auth.service'; @@ -18,12 +20,16 @@ export class AuthController { return this.authService.getUsers(); } - // @UseGuards(AdminAuthGuard) - // @Get('user/:userid') - // getUser(@Param('userid') userId: string): any { - // return this.authService.getUser(userId); - // } - + @UseGuards(JwtAuthGuard) + @Get('users/:userid') + getUser(@Param('userid') userId: string): any { + return this.authService.getUser(userId); + } + @UseGuards(JwtAuthGuard) + @Put('users/:userid') + updateKeycloakUser(@Body() keycloakUser: KeycloakUser): any { + return this.authService.updateKeycloakUser(keycloakUser); + } // @UseGuards(AdminAuthGuard) // @Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth // getLastLogin(@Param('userid') userId: string): any { diff --git a/bizmatch-server/src/auth/auth.service.ts b/bizmatch-server/src/auth/auth.service.ts index 518c1fd..6ce4893 100644 --- a/bizmatch-server/src/auth/auth.service.ts +++ b/bizmatch-server/src/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { KeycloakUser } from 'src/models/main.model'; - +import urlcat from 'urlcat'; @Injectable() export class AuthService { public async getAccessToken() { @@ -50,21 +50,40 @@ export class AuthService { const data = await response.json(); return data as KeycloakUser[]; } - // public async getUser(userid: string) { - // const token = await this.getAccessToken(); - // const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`; - // const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid }); - // const response = await ky - // .get(URL, { - // headers: { - // 'Content-Type': 'application/x-www-form-urlencoded', - // Authorization: `Bearer ${token}`, - // }, - // }) - // .json(); - // return response; - // } - + public async getUser(userid: string): Promise { + const token = await this.getAccessToken(); + const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`; + const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid }); + const response = await fetch(URL, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${token}`, + }, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + return data as KeycloakUser; + } + public async updateKeycloakUser(keycloakUser: KeycloakUser): Promise { + const token = await this.getAccessToken(); + const userid = keycloakUser.id; + const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`; + const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid }); + const response = await fetch(URL, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(keycloakUser), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } // public async getLastLogin(userid: string) { // const token = await this.getAccessToken(); // const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_LASTLOGIN_URL}`; diff --git a/bizmatch-server/src/drizzle/schema.ts b/bizmatch-server/src/drizzle/schema.ts index 2fa48e5..8d6b02b 100644 --- a/bizmatch-server/src/drizzle/schema.ts +++ b/bizmatch-server/src/drizzle/schema.ts @@ -134,8 +134,8 @@ export const commercials = pgTable( // }); export const listingEvents = pgTable('listing_events', { id: uuid('id').primaryKey().defaultRandom().notNull(), - listingId: uuid('listing_id'), // Assuming listings are referenced by UUID, adjust as necessary - email: varchar('email', { length: 255 }).references(() => users.email), + listingId: varchar('listing_id', { length: 255 }), // Assuming listings are referenced by UUID, adjust as necessary + email: varchar('email', { length: 255 }), eventType: varchar('event_type', { length: 50 }), // 'view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact' eventTimestamp: timestamp('event_timestamp').defaultNow(), userIp: varchar('user_ip', { length: 45 }), // Optional if you choose to track IP in frontend or backend diff --git a/bizmatch-server/src/models/db.model.ts b/bizmatch-server/src/models/db.model.ts index 292144f..c94c525 100644 --- a/bizmatch-server/src/models/db.model.ts +++ b/bizmatch-server/src/models/db.model.ts @@ -34,7 +34,7 @@ export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']); export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']); export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']); export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']); -export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite']); +export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite', 'emailus', 'pricing']); export type EventTypeEnum = z.infer; const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']); const TypeEnum = z.enum([ @@ -331,7 +331,7 @@ export type ShareByEMail = z.infer; export const ListingEventSchema = z.object({ id: z.string().uuid(), // UUID für das Event - listingId: z.string().uuid(), // UUID für das Listing + listingId: z.string().uuid().optional().nullable(), // UUID für das Listing email: z.string().email().optional().nullable(), // EMail des den Benutzer, optional, wenn kein Benutzer eingeloggt ist eventType: ZodEventTypeEnum, // Die Event-Typen eventTimestamp: z.string().datetime().or(z.date()), // Der Zeitstempel des Events, kann ein String im ISO-Format oder ein Date-Objekt sein diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index 83c094f..2a9aab4 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -120,6 +120,7 @@ export interface KeycloakUser { requiredActions?: any[]; notBefore?: number; access?: Access; + attributes?: Attributes; } export interface JwtUser { userId: string; @@ -128,6 +129,10 @@ export interface JwtUser { lastname: string; roles: string[]; } +interface Attributes { + [key: string]: any; + priceID: any; +} export interface Access { manageGroupMembership: boolean; view: boolean; @@ -174,6 +179,7 @@ export interface JwtToken { family_name: string; email: string; user_id: string; + price_id: string; } export interface JwtPayload { sub: string; diff --git a/bizmatch/src/app/pages/pricing/pricing.component.ts b/bizmatch/src/app/pages/pricing/pricing.component.ts index 784b465..a0ce7de 100644 --- a/bizmatch/src/app/pages/pricing/pricing.component.ts +++ b/bizmatch/src/app/pages/pricing/pricing.component.ts @@ -7,6 +7,7 @@ import { switchMap } from 'rxjs'; import { User } from '../../../../../bizmatch-server/src/models/db.model'; import { Checkout, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../environments/environment'; +import { AuditService } from '../../services/audit.service'; import { UserService } from '../../services/user.service'; import { SharedModule } from '../../shared/shared/shared.module'; import { map2User } from '../../utils/utils'; @@ -24,25 +25,50 @@ export class PricingComponent { pricingOverview: boolean | undefined = this.activatedRoute.snapshot.data['pricingOverview'] as boolean | undefined; keycloakUser: KeycloakUser; user: User; - constructor(public keycloakService: KeycloakService, private http: HttpClient, private stripeService: StripeService, private activatedRoute: ActivatedRoute, private userService: UserService, private router: Router) {} + constructor( + public keycloakService: KeycloakService, + private http: HttpClient, + private stripeService: StripeService, + private activatedRoute: ActivatedRoute, + private userService: UserService, + private router: Router, + private auditService: AuditService, + ) {} async ngOnInit() { const token = await this.keycloakService.getToken(); this.keycloakUser = map2User(token); if (this.keycloakUser) { - if (this.id === 'free') { - this.user = await this.userService.getByMail(this.keycloakUser.email); - this.user.subscriptionPlan = 'free'; - await this.userService.saveGuaranteed(this.user); - this.router.navigate([`/account`]); - } else if (this.id) { - this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` }); - } else if (!this.id && !this.pricingOverview) { - this.user = await this.userService.getByMail(this.keycloakUser.email); - if (this.user.subscriptionId) { + this.user = await this.userService.getByMail(this.keycloakUser.email); + const originalKeycloakUser = await this.userService.getKeycloakUser(this.keycloakUser.id); + const priceId = originalKeycloakUser.attributes && originalKeycloakUser.attributes['priceID'] ? originalKeycloakUser.attributes['priceID'][0] : null; + if (priceId) { + originalKeycloakUser.attributes['priceID'] = null; + await this.userService.updateKeycloakUser(originalKeycloakUser); + } + if (!this.user.subscriptionPlan) { + if (this.id === 'free' || priceId === 'free') { + this.user.subscriptionPlan = 'free'; + await this.userService.saveGuaranteed(this.user); this.router.navigate([`/account`]); + } else if (this.id || priceId) { + const base64PriceId = this.id ? this.id : priceId; + this.checkout({ priceId: atob(base64PriceId), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` }); } } + // if (this.id === 'free' || this.keycloakUser.priceId === 'free') { + // this.user.subscriptionPlan = 'free'; + // await this.userService.saveGuaranteed(this.user); + // this.router.navigate([`/account`]); + // } else if (this.id || this.keycloakUser.priceId) { + // const priceId = this.id ? this.id : this.keycloakUser.priceId; + // this.checkout({ priceId: atob(priceId), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` }); + // } else if (!this.id && !this.pricingOverview) { + // this.user = await this.userService.getByMail(this.keycloakUser.email); + // if (this.user.subscriptionId) { + // this.router.navigate([`/account`]); + // } + // } } else { this.pricingOverview = false; } 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 b0b9eed..9e02f22 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 @@ -3,12 +3,12 @@ import { KeycloakService } from 'keycloak-angular'; import { User } from '../../../../../../bizmatch-server/src/models/db.model'; import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; -import { environment } from '../../../../environments/environment'; import { MessageService } from '../../../components/message/message.service'; import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component'; import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component'; import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component'; import { ValidationMessagesService } from '../../../components/validation-messages.service'; +import { AuditService } from '../../../services/audit.service'; import { MailService } from '../../../services/mail.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; @@ -34,8 +34,9 @@ export class EmailUsComponent { private validationMessagesService: ValidationMessagesService, private messageService: MessageService, public selectOptions: SelectOptionsService, + private auditService: AuditService, ) { - this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; + this.mailinfo = createMailInfo(); } async ngOnInit() { const token = await this.keycloakService.getToken(); @@ -50,9 +51,11 @@ export class EmailUsComponent { } async mail() { try { + this.validationMessagesService.updateMessages([]); this.mailinfo.email = 'support@bizmatch.net'; await this.mailService.mail(this.mailinfo); this.messageService.addMessage({ severity: 'success', text: 'Your request has been forwarded to the support team of bizmatch.', duration: 3000 }); + this.auditService.createEvent(null, 'emailus', this.mailinfo.email, this.mailinfo); this.mailinfo = createMailInfo(this.user); } catch (error) { this.messageService.addMessage({ diff --git a/bizmatch/src/app/services/user.service.ts b/bizmatch/src/app/services/user.service.ts index 950b571..7bf6798 100644 --- a/bizmatch/src/app/services/user.service.ts +++ b/bizmatch/src/app/services/user.service.ts @@ -47,6 +47,12 @@ export class UserService { getNumberOfBroker(criteria?: UserListingCriteria): Observable { return this.http.post(`${this.apiBaseUrl}/bizmatch/user/findTotal`, criteria); } + getKeycloakUser(id: string): Promise { + return lastValueFrom(this.http.get(`${this.apiBaseUrl}/bizmatch/auth/users/${id}`)); + } + async updateKeycloakUser(keycloakUser: KeycloakUser): Promise { + await lastValueFrom(this.http.put(`${this.apiBaseUrl}/bizmatch/auth/users/${keycloakUser.id}`, keycloakUser)); + } // ------------------------------- // ADMIN SERVICES // ------------------------------- diff --git a/bizmatch/src/app/utils/utils.ts b/bizmatch/src/app/utils/utils.ts index a015444..f19cef3 100644 --- a/bizmatch/src/app/utils/utils.ts +++ b/bizmatch/src/app/utils/utils.ts @@ -137,9 +137,9 @@ export function resetUserListingCriteria(criteria: UserListingCriteria) { criteria.radius = null; } -export function createMailInfo(user: User): MailInfo { +export function createMailInfo(user?: User): MailInfo { return { - sender: { name: `${user.firstname} ${user.lastname}`, email: user.email, phoneNumber: user.phoneNumber, state: user.location?.state, comments: null }, + sender: user ? { name: `${user.firstname} ${user.lastname}`, email: user.email, phoneNumber: user.phoneNumber, state: user.location?.state, comments: null } : {}, email: null, url: environment.mailinfoUrl, listing: null,