einbau von rollen, neue Admin Ansicht

This commit is contained in:
2025-03-08 11:18:31 +01:00
parent dded8b8ca9
commit 5a56b3554d
29 changed files with 788 additions and 426 deletions

View File

@@ -68,7 +68,7 @@
<li>
<a routerLink="/account" (click)="closeDropdown()" 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">Account</a>
</li>
@if(user.customerType==='professional' || user.customerType==='seller' || isAdmin()){
@if(user.customerType==='professional' || user.customerType==='seller' || (authService.isAdmin() | async)){
<li>
@if(user.customerSubType==='broker'){
<a routerLink="/createBusinessListing" (click)="closeDropdown()" 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"
@@ -94,7 +94,7 @@
<a routerLink="/logout" (click)="closeDropdown()" 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">Logout</a>
</li>
</ul>
@if(isAdmin()){
@if(authService.isAdmin() | async){
<ul class="py-2">
<li>
<a routerLink="admin/users" (click)="closeDropdown()" 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">Users (Admin)</a>
@@ -121,8 +121,7 @@
>Properties</a
>
</li>
}
@if ((numberOfBroker$ | async) > 0) {
} @if ((numberOfBroker$ | async) > 0) {
<li>
<a
routerLink="/brokerListings"
@@ -165,8 +164,7 @@
>Properties</a
>
</li>
}
@if ((numberOfBroker$ | async) > 0) {
} @if ((numberOfBroker$ | async) > 0) {
<li>
<a
routerLink="/brokerListings"
@@ -219,8 +217,7 @@
>Properties</a
>
</li>
}
@if ((numberOfBroker$ | async) > 0) {
} @if ((numberOfBroker$ | async) > 0) {
<li>
<a
routerLinkActive="active-link"

View File

@@ -17,7 +17,7 @@ import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, isAdmin, map2User } from '../../utils/utils';
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service';
@UntilDestroy()
@@ -58,8 +58,8 @@ export class HeaderComponent {
private searchService: SearchService,
private criteriaChangeService: CriteriaChangeService,
public selectOptions: SelectOptionsService,
private authService: AuthService,
private listingService: ListingsService,
public authService: AuthService,
private listingService: ListingsService,
) {}
@HostListener('document:click', ['$event'])
handleGlobalClick(event: Event) {
@@ -76,7 +76,7 @@ export class HeaderComponent {
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
}
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
setTimeout(() => {
initFlowbite();
}, 10);
@@ -198,7 +198,4 @@ export class HeaderComponent {
toggleSortDropdown() {
this.sortDropdownVisible = !this.sortDropdownVisible;
}
isAdmin() {
return isAdmin(this.user.email);
}
}

View File

@@ -1,156 +1,154 @@
<!-- src/app/components/user-list/user-list.component.html -->
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Benutzerverwaltung</h1>
<div class="container mx-auto px-4 py-8 max-w-7xl">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Benutzerverwaltung</h2>
<!-- Ladeanzeige -->
<div *ngIf="isLoading" class="flex justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
<!-- Rollenfilter -->
<div class="mb-6">
<label for="roleFilter" class="block text-sm font-medium text-gray-700 mb-1">Nach Rolle filtern:</label>
<select id="roleFilter" class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" [(ngModel)]="selectedRole" (change)="onRoleFilterChange(selectedRole)">
<option value="all">Alle Benutzer</option>
<option value="admin">Admin</option>
<option value="pro">Pro</option>
<option value="guest">Guest</option>
<option [ngValue]="null">Keine Rolle</option>
</select>
</div>
<!-- Fehlermeldung -->
<div *ngIf="error" class="text-red-500 mb-4">
{{ error }}
<div *ngIf="error" class="bg-red-50 border border-red-200 text-red-800 rounded-md p-4 mb-6 relative">
<span class="block sm:inline">{{ error }}</span>
<button type="button" class="absolute top-4 right-4" (click)="error = null">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Benutzer-Tabelle -->
<table *ngIf="!isLoading && !error" class="min-w-full bg-white border">
<thead>
<tr>
<!-- <th class="py-2 px-4 border-b">ID</th> -->
<th class="py-2 px-4 border-b">Vorname</th>
<th class="py-2 px-4 border-b">Nachname</th>
<th class="py-2 px-4 border-b">E-Mail</th>
<th class="py-2 px-4 border-b">DB</th>
<th class="py-2 px-4 border-b">Keycloak</th>
<th class="py-2 px-4 border-b">Stripe</th>
<th class="py-2 px-4 border-b">Sub</th>
<th class="py-2 px-4 border-b">Aktionen</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of combinedUsers; let i = index" class="text-center">
<td class="py-2 px-4 border-b">
{{ user.appUser?.firstname || user.keycloakUser?.firstName || user.stripeUser?.name || '—' }}
</td>
<td class="py-2 px-4 border-b">
{{ user.appUser?.lastname || user.keycloakUser?.lastName || '—' }}
</td>
<td class="py-2 px-4 border-b">
{{ user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email }}
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.appUser" disabled />
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.keycloakUser" disabled />
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.stripeUser" disabled />
</td>
<td class="py-2 px-4 border-b">
@if(!!user.stripeSubscription){
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled attr.data-tooltip-target="tooltip-{{ i }}" />
}@else {
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled />
} @if(!!user.stripeSubscription){
<app-tooltip id="tooltip-{{ i }}" [text]="getSubscriptionInfo(user.stripeSubscription)"></app-tooltip>
}
</td>
<td class="py-2 px-4 border-b space-x-2">
<button class="share share-delete text-white font-bold text-xs py-1 px-2 inline-flex items-center" attr.data-dropdown-toggle="dropdown_{{ user.appUser?.id }}">
Delete<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" />
</svg>
</button>
<!-- Dropdown menu -->
<div id="dropdown_{{ user.appUser?.id }}" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton">
<li>
<a class="block px-4 py-2 hover:bg-gray-100" (click)="delete(user)">Complete</a>
</li>
@if(user.stripeSubscription){
<li>
<a class="block px-4 py-2 hover:bg-gray-100" (click)="deleteFromStripe(user)">From Stripe</a>
</li>
}
</ul>
</div>
<button class="share share-cc text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showCreditCardInfo(user)" [disabled]="!user.stripeSubscription">
<i class="fa-solid fa-credit-card"></i>&nbsp;CC Info
</button>
<button class="share share-msg text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showMessages(user)"><i class="fa-solid fa-message"></i>&nbsp;Messages</button>
</td>
</tr>
</tbody>
</table>
<!-- Ladeanzeige -->
<div *ngIf="loading" class="flex justify-center my-8">
<svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<!-- Flowbite Modal für Kreditkarteninformationen -->
<div *ngIf="showModal" class="fixed top-0 left-0 right-0 z-50 flex items-center justify-center w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full" aria-modal="true" role="dialog">
<div class="relative w-full max-w-2xl max-h-full">
<!-- Modal-Content -->
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Modal-Kopf -->
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Kreditkarteninformationen</h3>
<button
type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
(click)="closeModal()"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</button>
</div>
<!-- Modal-Körper -->
<div class="p-6 space-y-6">
<div *ngIf="ccInfoLoading" class="flex justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Benutzertabelle -->
<div class="overflow-x-auto shadow-md rounded-lg bg-white">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rolle</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail bestätigt</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Letzter Login</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr *ngFor="let user of users" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img *ngIf="user.photoURL" [src]="user.photoURL" alt="Profilbild" class="h-10 w-10 rounded-full" />
<div *ngIf="!user.photoURL" class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
<span class="text-gray-500 text-sm">{{ (user.displayName || user.email || '?').charAt(0).toUpperCase() }}</span>
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.displayName || 'Kein Name' }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ user.email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
[ngClass]="{
'bg-red-100 text-red-800': user.role === 'admin',
'bg-yellow-100 text-yellow-800': user.role === 'pro',
'bg-blue-100 text-blue-800': user.role === 'guest',
'bg-gray-100 text-gray-800': user.role === null
}"
>
{{ user.role || 'Keine' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div *ngIf="user.emailVerified" class="flex-shrink-0 h-5 w-5 text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div *ngIf="!user.emailVerified" class="flex-shrink-0 h-5 w-5 text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-2 text-sm text-gray-500">
{{ user.emailVerified ? 'Ja' : 'Nein' }}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.lastSignInTime | date : 'medium' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="relative" #dropdown>
<button (click)="dropdown.classList.toggle('active')" class="text-indigo-600 hover:text-indigo-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-50 rounded-md px-3 py-1 text-sm">
Rolle ändern
<svg class="h-4 w-4 inline-block ml-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div *ngIf="dropdown.classList.contains('active')" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div class="py-1" role="menu" aria-orientation="vertical">
<a (click)="changeUserRole(user, 'admin'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Admin</a>
<a (click)="changeUserRole(user, 'pro'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Pro</a>
<a (click)="changeUserRole(user, 'guest'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Guest</a>
<div class="border-t border-gray-100"></div>
<a (click)="changeUserRole(user, null); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Keine Rolle</a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="ccInfoError" class="text-red-500">
{{ ccInfoError }}
</div>
<div *ngIf="!ccInfoLoading && !ccInfoError">
<ng-container *ngIf="creditCardInfo.length > 0; else noCCInfo">
<table class="min-w-full bg-white border">
<thead>
<tr>
<th class="py-2 px-4 border-b">Kartenmarke</th>
<th class="py-2 px-4 border-b">Letzte 4</th>
<th class="py-2 px-4 border-b">Ablaufdatum</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let method of creditCardInfo" class="text-center">
<td class="py-2 px-4 border-b">{{ method.card?.brand || '—' }}</td>
<td class="py-2 px-4 border-b">{{ method.card?.last4 || '—' }}</td>
<td class="py-2 px-4 border-b">{{ method.card?.exp_month }}/{{ method.card?.exp_year }}</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #noCCInfo>
<p>Keine Kreditkarteninformationen verfügbar.</p>
</ng-template>
</div>
</div>
<!-- Modal-Fuß -->
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<button
type="button"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
(click)="closeModal()"
>
Schließen
</button>
</div>
<!-- Keine Benutzer gefunden -->
<div *ngIf="users.length === 0 && !loading" class="bg-blue-50 border border-blue-200 text-blue-800 rounded-md p-4 my-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">Keine Benutzer gefunden.</p>
</div>
</div>
</div>
<!-- "Mehr laden"-Button -->
<div *ngIf="hasMoreUsers" class="flex justify-center mt-6 mb-8">
<button
(click)="loadMoreUsers()"
[disabled]="loading"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
<svg *ngIf="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ loading ? 'Lädt...' : 'Weitere Benutzer laden' }}
</button>
</div>
</div>

View File

@@ -1,138 +1,97 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { PaymentMethod } from '@stripe/stripe-js';
import { initFlowbite } from 'flowbite';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { CombinedUser, StripeSubscription } from '../../../../../../bizmatch-server/src/models/main.model';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
import { MessageService } from '../../../components/message/message.service';
import { TooltipComponent } from '../../../components/tooltip/tooltip.component';
import { FormsModule } from '@angular/forms';
import { FirebaseUserInfo, UserRole, UsersResponse } from '../../../../../../bizmatch-server/src/models/main.model';
import { UserService } from '../../../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, TooltipComponent],
providers: [DatePipe],
templateUrl: './user-list.component.html',
styleUrl: './user-list.component.scss',
styleUrls: ['./user-list.component.scss'],
imports: [CommonModule, FormsModule],
standalone: true,
})
export class UserListComponent implements OnInit {
combinedUsers: CombinedUser[] = [];
isLoading = true;
users: FirebaseUserInfo[] = [];
loading = false;
error: string | null = null;
selectedUser: CombinedUser | null = null;
creditCardInfo: PaymentMethod[] = [];
ccInfoLoading = false;
ccInfoError: string | null = null;
showModal = false;
constructor(private userService: UserService, private datePipe: DatePipe, private confirmationService: ConfirmationService, private messageService: MessageService) {}
// Paginierung
pageToken?: string;
hasMoreUsers = false;
maxResultsPerPage = 50;
// Filterung
selectedRole: UserRole | 'all' = 'all';
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUsers();
}
ngAfterViewInit() {
// initFlowbite();
}
loadUsers(): void {
this.userService.loadUsers().subscribe({
next: users => {
this.combinedUsers = users;
this.isLoading = false;
setTimeout(() => {
initFlowbite();
}, 10);
},
error: err => {
this.error = 'Fehler beim Laden der Benutzer';
this.isLoading = false;
console.error(err);
},
});
}
getSubscriptionInfo(subscription: StripeSubscription) {
return `${subscription.metadata['plan']} / ${subscription.status} / ${this.datePipe.transform(new Date(subscription.start_date * 1000))} / ${this.datePipe.transform(
new Date(subscription.current_period_end * 1000),
)}`;
}
async deleteFromStripe(user: CombinedUser) {
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete the User from Stripe ?` });
if (confirmed) {
if (!user || !user.stripeUser) {
// Benutzer oder StripeUser nicht definiert
return;
}
this.loading = true;
this.error = null;
const customerId = user.stripeUser.id; // Angenommen, 'id' ist die Kunden-ID
try {
// 1. Stripe User löschen
await this.userService.deleteCustomerFromStripe(customerId);
console.log('Stripe User erfolgreich gelöscht.');
// 2. App-User aktualisieren
const appUser = user.appUser;
if (appUser) {
const updatedUser: User = {
...appUser,
subscriptionId: null,
customerType: 'buyer',
subscriptionPlan: 'free',
customerSubType: null,
};
const savedUser = await this.userService.saveGuaranteed(updatedUser);
console.log('App-User erfolgreich aktualisiert:', savedUser);
}
this.messageService.addMessage({
severity: 'success',
text: 'Stripe User deleted.',
duration: 3000, // 3 seconds
});
// Optional: Aktualisieren Sie die Benutzerliste oder führen Sie andere Aktionen aus
} catch (error) {
console.error('Fehler beim Löschen des Benutzers:', error);
this.messageService.addMessage({
severity: 'danger',
text: 'Error is occured during the deletion of the user ...',
duration: 3000, // 3 seconds
});
}
}
}
delete(user: CombinedUser): void {}
showCreditCardInfo(user: CombinedUser): void {
this.selectedUser = user;
this.creditCardInfo = [];
this.ccInfoError = null;
this.ccInfoLoading = true;
this.showModal = true;
const email = user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email;
if (email) {
this.userService.getPaymentMethods(email).subscribe({
next: methods => {
this.creditCardInfo = methods;
this.ccInfoLoading = false;
if (this.selectedRole !== 'all') {
// Benutzer nach Rolle filtern
this.userService.getUsersByRole(this.selectedRole).subscribe({
next: response => {
this.users = response.users;
this.loading = false;
this.hasMoreUsers = false; // Bei Rollenfilterung keine Paginierung
},
error: err => {
this.ccInfoError = 'Fehler beim Laden der Kreditkarteninformationen';
this.ccInfoLoading = false;
console.error(err);
this.error = 'Fehler beim Laden der Benutzer: ' + (err.message || err);
this.loading = false;
},
});
} else {
this.ccInfoError = 'Keine gültige E-Mail-Adresse gefunden';
this.ccInfoLoading = false;
// Alle Benutzer mit Paginierung laden
this.userService.getAllUsers(this.maxResultsPerPage, this.pageToken).subscribe({
next: (response: UsersResponse) => {
this.users = this.pageToken
? [...this.users, ...response.users] // Anhängen bei Paginierung
: response.users; // Ersetzen beim ersten Laden
this.pageToken = response.pageToken;
this.hasMoreUsers = !!response.pageToken;
this.loading = false;
},
error: err => {
this.error = 'Fehler beim Laden der Benutzer: ' + (err.message || err);
this.loading = false;
},
});
}
}
showMessages(user: CombinedUser): void {}
closeModal(): void {
this.showModal = false;
this.selectedUser = null;
this.creditCardInfo = [];
this.ccInfoError = null;
loadMoreUsers(): void {
if (this.hasMoreUsers && !this.loading) {
this.loadUsers();
}
}
onRoleFilterChange(role: UserRole | 'all'): void {
this.selectedRole = role;
this.users = []; // Liste zurücksetzen
this.pageToken = undefined; // Paginierung zurücksetzen
this.loadUsers();
}
changeUserRole(user: FirebaseUserInfo, newRole: UserRole): void {
this.userService.setUserRole(user.uid, newRole).subscribe({
next: () => {
// Benutzer in der lokalen Liste aktualisieren
const index = this.users.findIndex(u => u.uid === user.uid);
if (index !== -1) {
this.users[index] = { ...user, role: newRole };
}
},
error: err => {
this.error = `Fehler beim Ändern der Rolle für ${user.email}: ${err.message || err}`;
},
});
}
}

View File

@@ -24,7 +24,7 @@
</div>
</div>
<div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
@if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
<div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]">
<i class="fa-regular fa-pen-to-square"></i>

View File

@@ -21,7 +21,7 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, isAdmin, map2User } from '../../../utils/utils';
import { createMailInfo, map2User } from '../../../utils/utils';
// Import für Leaflet
// Benannte Importe für Leaflet
import { AuthService } from '../../../services/auth.service';
@@ -79,7 +79,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
private auditService: AuditService,
public emailService: EMailService,
private geoService: GeoService,
private authService: AuthService,
public authService: AuthService,
) {
super();
this.router.events.subscribe(event => {
@@ -115,9 +115,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
isAdmin() {
return isAdmin(this.keycloakUser?.email); //this.keycloakService.getUserRoles(true).includes('ADMIN');
}
async mail() {
try {
this.mailinfo.email = this.listingUser.email;

View File

@@ -20,7 +20,7 @@
</div>
</div>
<div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
@if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
<div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editCommercialPropertyListing', listing.id]">
<i class="fa-regular fa-pen-to-square"></i>

View File

@@ -24,7 +24,7 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, isAdmin, map2User } from '../../../utils/utils';
import { createMailInfo, map2User } from '../../../utils/utils';
import { BaseDetailsComponent } from '../base-details.component';
@Component({
@@ -83,7 +83,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
private messageService: MessageService,
private auditService: AuditService,
private emailService: EMailService,
private authService: AuthService,
public authService: AuthService,
) {
super();
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
@@ -139,9 +139,6 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
.catch(error => console.error('Error initializing Flowbite:', error));
});
}
isAdmin() {
return isAdmin(this.keycloakUser?.email);
}
async mail() {
try {
this.mailinfo.email = this.listingUser.email;

View File

@@ -137,7 +137,7 @@
</div>
}
</div>
} @if( user?.email===keycloakUser?.email || isAdmin()){
} @if( user?.email===keycloakUser?.email || (authService.isAdmin() | async)){
<button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" [routerLink]="['/account', user.id]">Edit</button>
}
</div>

View File

@@ -12,7 +12,7 @@ import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { formatPhoneNumber, isAdmin, map2User } from '../../../utils/utils';
import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-user',
@@ -45,7 +45,7 @@ export class DetailsUserComponent {
private sanitizer: DomSanitizer,
private imageService: ImageService,
public historyService: HistoryService,
private authService: AuthService,
public authService: AuthService,
) {}
async ngOnInit() {
@@ -59,8 +59,4 @@ export class DetailsUserComponent {
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : '');
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : '');
}
isAdmin() {
return isAdmin(this.user.email);
}
}

View File

@@ -86,7 +86,7 @@ export class HomeComponent {
initFlowbite();
}, 0);
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
const token = await this.authService.getToken();
sessionStorage.removeItem('businessListings');
sessionStorage.removeItem('commercialPropertyListings');

View File

@@ -9,7 +9,7 @@
<input type="email" id="email" name="email" [(ngModel)]="user.email" disabled class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
</div>
@if (isProfessional || isAdmin()){
@if (isProfessional || (authService.isAdmin() | async)){
<div class="flex flex-row items-center justify-around md:space-x-4">
<div class="flex h-full justify-between flex-col">
<p class="text-sm font-medium text-gray-700 mb-1">Company Logo</p>
@@ -71,7 +71,7 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</div> -->
@if (isAdmin() && !id){
@if ((authService.isAdmin() | async) && !id){
<div>
<label for="customerType" class="block text-sm font-medium text-gray-700">User Type</label>
<span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">ADMIN</span>

View File

@@ -33,7 +33,7 @@ import { SelectOptionsService } from '../../../services/select-options.service';
import { SharedService } from '../../../services/shared.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { isAdmin, map2User } from '../../../utils/utils';
import { map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'app-account',
@@ -96,7 +96,7 @@ export class AccountComponent {
// private subscriptionService: SubscriptionsService,
private datePipe: DatePipe,
private router: Router,
private authService: AuthService,
public authService: AuthService,
) {}
async ngOnInit() {
setTimeout(() => {
@@ -264,9 +264,7 @@ export class AccountComponent {
const message = this.validationMessages.find(msg => msg.field === fieldName);
return message ? message.message : '';
}
isAdmin() {
return isAdmin(this.user.email);
}
setState(index: number, state: string) {
if (state === null) {
this.user.areasServed[index].county = null;

View File

@@ -3,10 +3,12 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { FirebaseApp } from '@angular/fire/app';
import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
import { firstValueFrom } from 'rxjs';
import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs';
import { environment } from '../../environments/environment';
import { MailService } from './mail.service';
export type UserRole = 'admin' | 'pro' | 'guest';
@Injectable({
providedIn: 'root',
})
@@ -15,57 +17,90 @@ export class AuthService {
private auth = getAuth(this.app);
private http = inject(HttpClient);
private mailService = inject(MailService);
// Add a BehaviorSubject to track the current user role
private userRoleSubject = new BehaviorSubject<UserRole | null>(null);
public userRole$ = this.userRoleSubject.asObservable();
// Referenz für den gecachten API-Aufruf
private cachedUserRole$: Observable<UserRole | null> | null = null;
// Registrierung mit Email und Passwort
async registerWithEmail(email: string, password: string): Promise<UserCredential> {
// Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL
let verificationUrl = '';
// Prüfen der aktuellen Umgebung basierend auf dem Host
const currentHost = window.location.hostname;
if (currentHost.includes('localhost')) {
verificationUrl = 'http://localhost:4200/email-authorized';
} else if (currentHost.includes('dev.bizmatch.net')) {
verificationUrl = 'https://dev.bizmatch.net/email-authorized';
} else {
verificationUrl = 'https://www.bizmatch.net/email-authorized';
// Zeitraum in ms, nach dem der Cache zurückgesetzt werden soll (z.B. 5 Minuten)
private cacheDuration = 5 * 60 * 1000;
private lastCacheTime = 0;
constructor() {
// Load role from token when service is initialized
this.loadRoleFromToken();
}
// ActionCode-Einstellungen mit der dynamischen URL
const actionCodeSettings = {
url: `${verificationUrl}?email=${email}`,
handleCodeInApp: true
};
// Benutzer erstellen
const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
// E-Mail-Verifizierung mit den angepassten ActionCode-Einstellungen senden
if (userCredential.user) {
//await sendEmailVerification(userCredential.user, actionCodeSettings);
this.mailService.sendVerificationEmail(userCredential.user.email).subscribe({
next: () => {
console.log('Verification email sent successfully');
// Erfolgsmeldung anzeigen
},
error: (error) => {
console.error('Error sending verification email', error);
// Fehlermeldung anzeigen
private loadRoleFromToken(): void {
this.getToken().then(token => {
if (token) {
const role = this.extractRoleFromToken(token);
this.userRoleSubject.next(role);
} else {
this.userRoleSubject.next(null);
}
});
}
// Token, RefreshToken und ggf. photoURL speichern
const token = await userCredential.user.getIdToken();
localStorage.setItem('authToken', token);
localStorage.setItem('refreshToken', userCredential.user.refreshToken);
if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL);
private extractRoleFromToken(token: string): UserRole | null {
try {
const payloadBase64 = token.split('.')[1];
const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/'));
const payload = JSON.parse(payloadJson);
return (payload.role as UserRole) || null;
} catch (e) {
return null;
}
}
// Registrierung mit Email und Passwort
async registerWithEmail(email: string, password: string): Promise<UserCredential> {
// Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL
let verificationUrl = '';
return userCredential;
}
// Prüfen der aktuellen Umgebung basierend auf dem Host
const currentHost = window.location.hostname;
if (currentHost.includes('localhost')) {
verificationUrl = 'http://localhost:4200/email-authorized';
} else if (currentHost.includes('dev.bizmatch.net')) {
verificationUrl = 'https://dev.bizmatch.net/email-authorized';
} else {
verificationUrl = 'https://www.bizmatch.net/email-authorized';
}
// ActionCode-Einstellungen mit der dynamischen URL
const actionCodeSettings = {
url: `${verificationUrl}?email=${email}`,
handleCodeInApp: true,
};
// Benutzer erstellen
const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
// E-Mail-Verifizierung mit den angepassten ActionCode-Einstellungen senden
if (userCredential.user) {
//await sendEmailVerification(userCredential.user, actionCodeSettings);
this.mailService.sendVerificationEmail(userCredential.user.email).subscribe({
next: () => {
console.log('Verification email sent successfully');
// Erfolgsmeldung anzeigen
},
error: error => {
console.error('Error sending verification email', error);
// Fehlermeldung anzeigen
},
});
}
// const token = await userCredential.user.getIdToken();
// localStorage.setItem('authToken', token);
// localStorage.setItem('refreshToken', userCredential.user.refreshToken);
// if (userCredential.user.photoURL) {
// localStorage.setItem('photoURL', userCredential.user.photoURL);
// }
return userCredential;
}
// Login mit Email und Passwort
loginWithEmail(email: string, password: string): Promise<UserCredential> {
@@ -77,6 +112,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL);
}
this.loadRoleFromToken();
}
return userCredential;
});
@@ -93,6 +129,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL);
}
this.loadRoleFromToken();
}
return userCredential;
});
@@ -103,9 +140,74 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('photoURL');
this.clearRoleCache();
this.userRoleSubject.next(null);
return this.auth.signOut();
}
isAdmin(): Observable<boolean> {
return this.getUserRole().pipe(
map(role => role === 'admin'),
// take(1) ist optional - es beendet die Subscription, nachdem ein Wert geliefert wurde
// Nützlich, wenn du die Methode in einem Template mit dem async pipe verwendest
take(1),
);
}
// Get current user's role from the server with caching
getUserRole(): Observable<UserRole | null> {
const now = Date.now();
// Cache zurücksetzen, wenn die Caching-Zeit abgelaufen ist oder kein Cache existiert
if (!this.cachedUserRole$ || now - this.lastCacheTime > this.cacheDuration) {
this.lastCacheTime = now;
this.cachedUserRole$ = this.http.get<{ role: UserRole | null }>(`${environment.apiBaseUrl}/bizmatch/auth/me/role`).pipe(
map(response => response.role),
tap(role => this.userRoleSubject.next(role)),
catchError(error => {
console.error('Error fetching user role', error);
return of(null);
}),
// Cache für mehrere Subscriber und behalte den letzten Wert
// Der Parameter 1 gibt an, dass der letzte Wert gecacht werden soll
// refCount: false bedeutet, dass der Cache nicht zurückgesetzt wird, wenn keine Subscriber mehr da sind
shareReplay({ bufferSize: 1, refCount: false }),
);
}
return this.cachedUserRole$;
}
clearRoleCache(): void {
this.cachedUserRole$ = null;
this.lastCacheTime = 0;
}
// Check if user has a specific role
hasRole(role: UserRole): Observable<boolean> {
return this.userRole$.pipe(
map(userRole => {
if (role === 'guest') {
// Any authenticated user can access guest features
return userRole !== null;
} else if (role === 'pro') {
// Both pro and admin can access pro features
return userRole === 'pro' || userRole === 'admin';
} else if (role === 'admin') {
// Only admin can access admin features
return userRole === 'admin';
}
return false;
}),
);
}
// Force refresh the token to get updated custom claims
async refreshUserClaims(): Promise<void> {
this.clearRoleCache();
if (this.auth.currentUser) {
await this.auth.currentUser.getIdToken(true);
const token = await this.auth.currentUser.getIdToken();
localStorage.setItem('authToken', token);
this.loadRoleFromToken();
}
}
// Prüft, ob ein Token noch gültig ist (über die "exp"-Eigenschaft)
private isTokenValid(token: string): boolean {
try {

View File

@@ -1,10 +1,10 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { PaymentMethod } from '@stripe/stripe-js';
import { catchError, forkJoin, lastValueFrom, map, Observable, of, Subject } from 'rxjs';
import urlcat from 'urlcat';
import { User } from '../../../../bizmatch-server/src/models/db.model';
import { CombinedUser, KeycloakUser, ResponseUsersArray, StripeSubscription, StripeUser, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
import { CombinedUser, FirebaseUserInfo, KeycloakUser, ResponseUsersArray, StripeSubscription, StripeUser, UserListingCriteria, UserRole, UsersResponse } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
@@ -56,6 +56,41 @@ export class UserService {
// -------------------------------
// ADMIN SERVICES
// -------------------------------
/**
* Ruft alle Benutzer mit Paginierung ab
*/
getAllUsers(maxResults?: number, pageToken?: string): Observable<UsersResponse> {
let params = new HttpParams();
if (maxResults) {
params = params.set('maxResults', maxResults.toString());
}
if (pageToken) {
params = params.set('pageToken', pageToken);
}
return this.http.get<UsersResponse>(`${this.apiBaseUrl}/bizmatch/auth`, { params });
}
/**
* Ruft Benutzer mit einer bestimmten Rolle ab
*/
getUsersByRole(role: UserRole): Observable<{ users: FirebaseUserInfo[] }> {
return this.http.get<{ users: FirebaseUserInfo[] }>(`${this.apiBaseUrl}/bizmatch/auth/role/${role}`);
}
/**
* Ändert die Rolle eines Benutzers
*/
setUserRole(uid: string, role: UserRole): Observable<{ success: boolean }> {
return this.http.post<{ success: boolean }>(`${this.apiBaseUrl}/${uid}/bizmatch/auth/role`, { role });
}
// -------------------------------
// OLDADMIN SERVICES
// -------------------------------
getKeycloakUsers(): Observable<KeycloakUser[]> {
return this.http.get<KeycloakUser[]>(`${this.apiBaseUrl}/bizmatch/auth/user/all`).pipe(
catchError(error => {

View File

@@ -340,6 +340,6 @@ export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPro
}
});
}
export function isAdmin(email: string) {
return 'andreas.knuth@gmail.com' === email;
}
// export function isAdmin(email: string) {
// return 'andreas.knuth@gmail.com' === email;
// }