first commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-container button {
|
||||
background-color: rgba(0, 0, 0, 0.25); ;
|
||||
color: #FFF;
|
||||
border-radius: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-right: 5px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.profile-photo-upload-container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="profile-photo-upload-container" style="margin-bottom: 40px;">
|
||||
<!-- Hidden file input for styling purposes -->
|
||||
<input type="file" #fileInput hidden (change)="onFileSelected($event)" accept="image/*">
|
||||
<div class="upload-container">
|
||||
<button *ngIf="!currentUser?.photoUrl" (click)="fileSelected ? uploadFile() : fileInput.click()" class="image-upload-button">
|
||||
<mat-icon>{{ fileSelected ? 'upload_file' : 'image' }}</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="currentUser?.photoUrl" mat-fab (click)="deleteFile()" class="close-icon-button">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProfilePhotoUploadComponent } from './profile-photo-upload.component';
|
||||
|
||||
describe('ProfilePhotoUploadComponent', () => {
|
||||
let component: ProfilePhotoUploadComponent;
|
||||
let fixture: ComponentFixture<ProfilePhotoUploadComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProfilePhotoUploadComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProfilePhotoUploadComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UserService } from '../../user.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { User } from '../../../auth/model/user.model';
|
||||
import { ProfilePhotoUploadService } from './profile-photo-upload.service';
|
||||
import { AngularMaterialModule } from '../../../angular-material/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-photo-upload',
|
||||
standalone: true,
|
||||
imports: [AngularMaterialModule, CommonModule],
|
||||
templateUrl: './profile-photo-upload.component.html',
|
||||
styleUrls: ['./profile-photo-upload.component.css'],
|
||||
})
|
||||
export class ProfilePhotoUploadComponent implements OnInit, OnDestroy {
|
||||
//TODO check why the userId remains empty/null in component, but in service the uid is returned proper?
|
||||
userProfileSubscription!: Subscription;
|
||||
currentUser: User | null = null;
|
||||
fileSelected: boolean = false;
|
||||
metadata = {
|
||||
contentType: 'image/jpeg',
|
||||
customMetadata: {
|
||||
imgId: undefined,
|
||||
},
|
||||
};
|
||||
selectedFile: File | null = null;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private profilePhotoUploadService: ProfilePhotoUploadService,
|
||||
private notifierService: NotifierService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userProfileSubscription =
|
||||
this.userService.currentUserProfile$.subscribe((user: User | null) => {
|
||||
this.currentUser = user;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.userProfileSubscription) {
|
||||
this.userProfileSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
onFileSelected(event: any) {
|
||||
this.selectedFile = event.target.files[0];
|
||||
this.fileSelected = true;
|
||||
this.notifierService.showNotification(
|
||||
`Datei wurde ausgewählt. Bitte klicke auf die Schaltfläche "Hochladen", um Bild einzustellen.`,
|
||||
'OK'
|
||||
);
|
||||
}
|
||||
|
||||
async uploadFile() {
|
||||
if (this.selectedFile && this.currentUser && this.currentUser.uid) {
|
||||
const file = this.selectedFile;
|
||||
const metadata = {
|
||||
contentType: 'image/jpeg',
|
||||
customMetadata: {
|
||||
imgId: this.currentUser?.uid,
|
||||
},
|
||||
};
|
||||
await this.profilePhotoUploadService.uploadFile(
|
||||
this.currentUser.uid,
|
||||
file,
|
||||
metadata
|
||||
);
|
||||
this.notifierService.showNotification(`Datei wurde hochgeladen`, 'OK');
|
||||
} else {
|
||||
this.notifierService.showInformation(
|
||||
'Keine Datei ausgewählt oder Benutzer-ID ist unbekannt.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile() {
|
||||
if (this.currentUser && this.currentUser.uid) {
|
||||
await this.profilePhotoUploadService.deleteFile(this.currentUser.uid);
|
||||
this.notifierService.showNotification(`Datei wurde gelöscht`, 'OK');
|
||||
} else {
|
||||
this.notifierService.showInformation('Benutzer-ID ist unbekannt. Bitte melde dich an, um die Datei zu löschen.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
getStorage,
|
||||
ref,
|
||||
uploadBytesResumable,
|
||||
getDownloadURL,
|
||||
deleteObject,
|
||||
} from 'firebase/storage';
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
setDoc,
|
||||
getDoc,
|
||||
deleteDoc,
|
||||
updateDoc,
|
||||
} from 'firebase/firestore';
|
||||
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProfilePhotoUploadService {
|
||||
storage = getStorage();
|
||||
firestore = getFirestore();
|
||||
|
||||
constructor(private notifierService: NotifierService) {}
|
||||
|
||||
async uploadFile(userId: string, file: File, metadata: any): Promise<void> {
|
||||
// get the current user's file metadata from firestore
|
||||
const userDocRef = doc(this.firestore, 'users', userId);
|
||||
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||
const fileDoc = await getDoc(fileDocRef);
|
||||
|
||||
if (fileDoc.exists()) {
|
||||
// if there's an existing file, delete it from storage
|
||||
const existingFilePath = fileDoc.data()['filePath'];
|
||||
const existingFileRef = ref(this.storage, existingFilePath);
|
||||
await deleteObject(existingFileRef).catch((error) => {
|
||||
(`Error deleting existing file: ${{error}}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (file.size > 30 * 1024) {
|
||||
// If the file is too large, compress the image
|
||||
this.notifierService.showInformation('File size exceeds 30 KB. Compressing the image...')
|
||||
file = await this.compressImage(file);
|
||||
}
|
||||
|
||||
//creates a firebase storage for images
|
||||
const filePath = `images/${userId}/${file.name}`;
|
||||
const storageRef = ref(this.storage, filePath);
|
||||
|
||||
//uploads the image to the storage
|
||||
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
|
||||
uploadTask.on(
|
||||
'state_changed',
|
||||
//show the upload process in the console
|
||||
(snapshot) => {
|
||||
switch (snapshot.state) {
|
||||
case 'error':
|
||||
this.notifierService.showInformation('Upload failed')
|
||||
break;
|
||||
case 'success':
|
||||
this.notifierService.showInformation('Upload completed')
|
||||
break;
|
||||
}
|
||||
},
|
||||
//error handling, display currently only in the console
|
||||
(error) => {
|
||||
switch (error.code) {
|
||||
case 'storage/unauthorized':
|
||||
this.notifierService.showInformation('The user is not authenticated. Please authenticate yourself and try again.');
|
||||
break;
|
||||
case 'storage/canceled':
|
||||
this.notifierService.showInformation('The user has cancelled the process.');
|
||||
break;
|
||||
case 'storage/unknown':
|
||||
this.notifierService.showInformation('An unknown error has occurred.');
|
||||
break;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//returns the download url of the uploaded image
|
||||
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||
const fileMetadata = {
|
||||
id: userId,
|
||||
filePath: filePath,
|
||||
downloadURL: downloadURL,
|
||||
};
|
||||
// Set the file metadata in the 'files' collection
|
||||
setDoc(fileDocRef, fileMetadata).catch((error) => {
|
||||
this.notifierService.showInformation(`Error updating file${{error}}`);
|
||||
});
|
||||
// Update the user's document with the new photoUrl
|
||||
updateDoc(userDocRef, { photoUrl: downloadURL })
|
||||
.catch((error) => {
|
||||
this.notifierService.showInformation(`Error updating user document with photo URL: ${{error}}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async deleteFile(userId: string): Promise<void> {
|
||||
try {
|
||||
// Get the current user's file metadata from Firestore
|
||||
const userDocRef = doc(this.firestore, 'users', userId);
|
||||
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||
const fileDoc = await getDoc(fileDocRef);
|
||||
if (fileDoc.exists()) {
|
||||
// If there's an existing file, delete it from storage
|
||||
const existingFilePath = fileDoc.data()['filePath'];
|
||||
const existingFileRef = ref(this.storage, existingFilePath);
|
||||
await deleteObject(existingFileRef);
|
||||
await deleteDoc(fileDocRef);
|
||||
updateDoc(userDocRef, { photoUrl: null })
|
||||
.catch((error) => {
|
||||
this.notifierService.showInformation(`Error removing photo URL from user document: ${{error}}`);
|
||||
});
|
||||
} else {
|
||||
this.notifierService.showInformation('No file metadata found to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifierService.showInformation(`Error deleting file: ${{error}}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compress an image
|
||||
private compressImage(file: File): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(file);
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
const maxWidth = 1024; // Set the maximum width you want for the images
|
||||
const maxHeight = 1024; // Set the maximum height you want for the images
|
||||
let width = image.width;
|
||||
let height = image.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > maxWidth) {
|
||||
height *= maxWidth / width;
|
||||
width = maxWidth;
|
||||
}
|
||||
} else {
|
||||
if (height > maxHeight) {
|
||||
width *= maxHeight / height;
|
||||
height = maxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
|
||||
// Function to attempt to compress the image
|
||||
const attemptCompression = (quality: number, resolve: (file: File) => void, reject: (reason: Error) => void) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob === null) {
|
||||
reject(new Error('Failed to create blob from canvas'));
|
||||
return;
|
||||
}
|
||||
if (blob.size <= 30 * 1024) {
|
||||
// If the blob size is less than or equal to 100 KB, resolve the promise
|
||||
const compressedFile = new File([blob], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
resolve(compressedFile);
|
||||
} else if (quality > 0.1) {
|
||||
// If the quality can still be reduced, try again with lower quality
|
||||
attemptCompression(quality - 0.1, resolve, reject);
|
||||
} else {
|
||||
// If the quality is already too low, reject the promise
|
||||
reject(new Error('Cannot compress image to the desired size'));
|
||||
}
|
||||
},
|
||||
'image/jpeg',
|
||||
quality
|
||||
);
|
||||
};
|
||||
|
||||
// Start the compression attempt with an initial quality value
|
||||
attemptCompression(0.5, resolve, reject);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
reject(new Error('Image loading failed'));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
214
src/app/user-profile/user-profile.component.css
Normal file
214
src/app/user-profile/user-profile.component.css
Normal file
@@ -0,0 +1,214 @@
|
||||
.flex-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh; /* Full viewport height */
|
||||
}
|
||||
|
||||
/* Form */
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flex3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
mat-card {
|
||||
padding: 2.5rem;
|
||||
width: min-content;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Form Fields */
|
||||
mat-form-field {
|
||||
width: 17.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Card Footer */
|
||||
mat-card-footer {
|
||||
color: #0596ff;
|
||||
text-align: center;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
#logOutButton {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
width: 15.9375rem;
|
||||
height: 3.75rem;
|
||||
background-color: #002d3c;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6.25rem;
|
||||
margin-top: 3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Button-Disabled */
|
||||
button[disabled] {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
::ng-deep
|
||||
.mdc-text-field--outlined.mdc-text-field--disabled
|
||||
.mdc-text-field__input {
|
||||
color: #000010 !important;
|
||||
}
|
||||
|
||||
::ng-deep
|
||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined
|
||||
.mdc-notched-outline--upgraded
|
||||
.mdc-floating-label--float-above {
|
||||
color: #949494 !important;
|
||||
}
|
||||
|
||||
/* Spoiler text for the Change Password toggle */
|
||||
.change-password-toggle {
|
||||
display: flex;
|
||||
justify-content: center; /* Center the text horizontally */
|
||||
margin-top: 1rem; /* Add space above the text */
|
||||
margin-bottom: 0.5rem; /* Reduce space below the text for closer proximity to fields */
|
||||
cursor: pointer; /* Change cursor to indicate clickable text */
|
||||
}
|
||||
|
||||
.change-password-toggle p {
|
||||
color: #002d3c; /* Set the text color */
|
||||
text-decoration: underline; /* Underline the text to indicate it's clickable */
|
||||
}
|
||||
|
||||
.change-password-toggle p:hover {
|
||||
color: #0596ff; /* Change text color on hover for visual feedback */
|
||||
}
|
||||
|
||||
/* Container for the Change Password form fields */
|
||||
.change-password-fields {
|
||||
display: flex;
|
||||
flex-direction: column; /* Stack the form fields vertically */
|
||||
gap: 1.5rem; /* Space between each form field */
|
||||
}
|
||||
|
||||
/* Style for each form field */
|
||||
.change-password-fields mat-form-field {
|
||||
margin-bottom: 1.5rem; /* Add space below each form field */
|
||||
}
|
||||
|
||||
/* Style for the Update Password button */
|
||||
.update-password-button {
|
||||
display: flex;
|
||||
justify-content: center; /* Center the button horizontally */
|
||||
margin-top: 0.5rem; /* Space above the button, consistent with form field spacing */
|
||||
}
|
||||
|
||||
.update-password-button button {
|
||||
padding: 0.5rem 1rem; /* Increase padding for more space around the text */
|
||||
font-size: 0.75rem; /* Smaller font size for the button text */
|
||||
color: #333; /* Change text color for the button */
|
||||
background-color: transparent; /* No background color for the button */
|
||||
border: 1px solid #002d3c; /* Add a border with the desired color */
|
||||
border-radius: 0.25rem; /* Slight rounding of corners */
|
||||
cursor: pointer; /* Change cursor to indicate clickable button */
|
||||
text-transform: uppercase; /* Uppercase button text */
|
||||
letter-spacing: 0.05rem; /* Add some letter spacing */
|
||||
transition: all 0.3s; /* Transition for color and border color change */
|
||||
box-shadow: none; /* Remove the shadow for a flat style */
|
||||
font-weight: bold; /* Make the text bold */
|
||||
}
|
||||
|
||||
/* Override padding for suffix icon in LTR direction */
|
||||
:host ::ng-deep .mat-mdc-form-field-icon-suffix {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Override padding for prefix icon in RTL direction */
|
||||
:host ::ng-deep [dir="rtl"] .mat-mdc-form-field-icon-prefix {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.userProfileDiv {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
max-width: 17.8rem;
|
||||
max-height: 17.8rem;
|
||||
border-radius: 10%;
|
||||
border: 1px solid #0011;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.userProfileImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sync-icon {
|
||||
margin-top: 13px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.spinner-section {
|
||||
margin-top: 13px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner-section-center {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.non-editable-field ::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background: transparent; /* Setzt die Farbe des Labels auf inaktiv */
|
||||
}
|
||||
|
||||
.non-editable-field input[readonly] {
|
||||
color: #000000; /*rgba(0, 0, 0, 0.4); Graue Textfarbe für nicht bearbeitbare Eingabe */
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
margin: 0 20%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.center-container {
|
||||
margin: 0 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.default-background-color {
|
||||
background: #f6f9fc;
|
||||
}
|
||||
|
||||
.editable-field {
|
||||
width: 13.8rem;
|
||||
}
|
||||
176
src/app/user-profile/user-profile.component.html
Normal file
176
src/app/user-profile/user-profile.component.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<app-header></app-header>
|
||||
<!-- Flex container -->
|
||||
<div class="flex" *ngIf="dataLoaded && showProfile">
|
||||
<!-- Centered container -->
|
||||
<div class="center-container">
|
||||
<!-- Material card -->
|
||||
<div class="user-info">
|
||||
<mat-card class="default-background-color">
|
||||
<!-- E-Mail verification warn card -->
|
||||
<app-verify-email-warn-card
|
||||
*ngIf="isProfileOwner"
|
||||
></app-verify-email-warn-card>
|
||||
|
||||
<!-- Profile photo based on user -->
|
||||
<div class="m-auto">
|
||||
<div
|
||||
*ngIf="
|
||||
userProfilePhoto && userProfilePhoto != '';
|
||||
else defaultAvatar
|
||||
"
|
||||
class="m-auto userProfileDiv"
|
||||
>
|
||||
<img
|
||||
class="userProfileImg"
|
||||
src="{{ userProfilePhoto }}"
|
||||
alt="User Foto"
|
||||
/>
|
||||
<!-- Include the profile photo upload component here -->
|
||||
<app-profile-photo-upload
|
||||
*ngIf="isProfileOwner"
|
||||
></app-profile-photo-upload>
|
||||
</div>
|
||||
<!-- Conditional SVG based on user's email domain -->
|
||||
<ng-template #defaultAvatar>
|
||||
<div class="userProfileDiv" style="margin-bottom: 40px">
|
||||
<ng-container *ngIf="isCompanyAtos(); else otherAvatar">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 295 295"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="147.5"
|
||||
cy="122.917"
|
||||
r="36.875"
|
||||
stroke="#0596FF"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M221.25 229.927C216.9 216.859 207.314 205.311 193.98 197.075C180.646 188.839 164.308 184.375 147.5 184.375C130.692 184.375 114.354 188.839 101.02 197.075C87.6857 205.311 78.1001 216.859 73.75 229.927"
|
||||
stroke="#0596FF"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-template #otherAvatar>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 295 295"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="147.5"
|
||||
cy="122.917"
|
||||
r="36.875"
|
||||
stroke="#FF6D43"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M221.25 229.927C216.9 216.859 207.314 205.311 193.98 197.075C180.646 188.839 164.308 184.375 147.5 184.375C130.692 184.375 114.354 188.839 101.02 197.075C87.6857 205.311 78.1001 216.859 73.75 229.927"
|
||||
stroke="#FF6D43"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ng-template>
|
||||
<!-- Include the profile photo upload component here as well -->
|
||||
<app-profile-photo-upload
|
||||
*ngIf="isProfileOwner"
|
||||
></app-profile-photo-upload>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- Conditional SVG based on user's email domain -->
|
||||
|
||||
<!-- Card content -->
|
||||
<mat-card-content>
|
||||
<form [formGroup]="userProfileForm" (ngSubmit)="saveProfile()">
|
||||
<!-- Name input field -->
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="non-editable-field">
|
||||
<mat-label>Name</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[readonly]="true"
|
||||
formControlName="name"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Email input field -->
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="non-editable-field">
|
||||
<mat-label>Email</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[readonly]="true"
|
||||
formControlName="email"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex3"
|
||||
*ngIf="isProfileOwner || userProfileForm.value.firmenposition"
|
||||
>
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="{{
|
||||
isProfileOwner ? 'editable-field' : 'non-editable-field'
|
||||
}}"
|
||||
>
|
||||
<mat-label>Firmenposition</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[readonly]="!isProfileOwner"
|
||||
formControlName="firmenposition"
|
||||
placeholder="z. B. Developer"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<!-- Save button -->
|
||||
<mat-icon
|
||||
class="sync-icon"
|
||||
*ngIf="isProfileOwner && !showSpinnerModule"
|
||||
(click)="saveProfile()"
|
||||
>sync</mat-icon
|
||||
>
|
||||
<app-progress-spinner
|
||||
class="spinner-section"
|
||||
[showSpinner]="showSpinnerModule"
|
||||
></app-progress-spinner>
|
||||
</div>
|
||||
|
||||
<!-- User wins -->
|
||||
<div class="flex3">
|
||||
<h2>Siege:</h2>
|
||||
<!-- Display user wins if available, otherwise show loading or blank -->
|
||||
<h1 [style.color]="isCompanyAtos() ? '#0596ff' : '#ff6d43'">
|
||||
{{
|
||||
userProfileForm.controls["wins"].value
|
||||
? userProfileForm.controls["wins"].value
|
||||
: 0
|
||||
}}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- ... other parts of your form ... -->
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-progress-spinner
|
||||
class="spinner-section-center"
|
||||
[showSpinner]="!dataLoaded"
|
||||
></app-progress-spinner>
|
||||
393
src/app/user-profile/user-profile.component.ts
Normal file
393
src/app/user-profile/user-profile.component.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { AngularMaterialModule } from '../angular-material/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { Router, RouterModule, ActivatedRoute } from '@angular/router';
|
||||
import { UserService } from './user.service';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { AuthService } from '../auth/authService/auth.service';
|
||||
import { Subscription, Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { User } from '../auth/model/user.model';
|
||||
import { ScoreTableService } from '../score-table/score-table.service';
|
||||
import { PendingActionsService } from '../score/pending-actions.service';
|
||||
import { NotifierService } from '../shared/notifierService/notifier.service';
|
||||
import { ProfilePhotoUploadComponent } from './profile-photo/profile-photo-upload/profile-photo-upload.component';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { ProgressSpinnerComponent } from '../shared/progress-spinner/progress-spinner/progress-spinner.component';
|
||||
import { HeaderComponent } from '../shared/header/header.component';
|
||||
import {
|
||||
PasswordValidationType,
|
||||
passwordValidator,
|
||||
} from '../shared/validation/passwordMatcherValidation';
|
||||
import {
|
||||
hasNumberValidator,
|
||||
hasUpperCaseValidator,
|
||||
} from '../shared/validation/passwordSaferValidation';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { VerifiyWarnCardComponent } from './verify-email-warn-card/verify-email-warn-card.component';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-test',
|
||||
standalone: true,
|
||||
providers: [UserService, AuthService],
|
||||
templateUrl: './user-profile.component.html',
|
||||
styleUrl: './user-profile.component.css',
|
||||
imports: [
|
||||
CommonModule,
|
||||
AngularMaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
HttpClientModule,
|
||||
ProfilePhotoUploadComponent,
|
||||
MatDividerModule,
|
||||
ProgressSpinnerComponent,
|
||||
HeaderComponent,
|
||||
MatProgressSpinner,
|
||||
VerifiyWarnCardComponent,
|
||||
],
|
||||
})
|
||||
export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
userProfileForm!: FormGroup; // Form group to manage user profile data
|
||||
userProfilePhoto: string | undefined | null;
|
||||
userProfileSubscription!: Subscription; // Subscription to user profile changes
|
||||
user$ = this.userService.currentUserProfile$; // Observable of current user profile data
|
||||
dataLoaded: boolean = false;
|
||||
showProfile: boolean = true;
|
||||
hide: boolean = true;
|
||||
showSpinnerModule: boolean = false;
|
||||
isProfileOwner: boolean = false;
|
||||
isUpdatingPassword: boolean = false;
|
||||
private passwordValueChangesSubscription?: Subscription;
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
showChangePassword: boolean = false;
|
||||
passwordForm: FormGroup;
|
||||
errorMessageCurrentPassword = '';
|
||||
errorMessageNewPassword = '';
|
||||
errorMessageConfirmPassword = '';
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private scoreTableService: ScoreTableService,
|
||||
private pendingActionsService: PendingActionsService,
|
||||
private notifierService: NotifierService
|
||||
) {
|
||||
this.dataLoaded = false;
|
||||
// Initialize the password form with form controls and validators from the new branch
|
||||
this.passwordForm = new FormGroup({
|
||||
currentPassword: new FormControl('', [Validators.required]),
|
||||
newPassword: new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(10),
|
||||
Validators.maxLength(20),
|
||||
hasUpperCaseValidator(),
|
||||
hasNumberValidator(),
|
||||
]),
|
||||
confirmNewPassword: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
// Apply the non-matching validator to the newPassword control
|
||||
const newPasswordControl = this.passwordForm.get('newPassword');
|
||||
const currentPasswordControl = this.passwordForm.get('currentPassword');
|
||||
const confirmNewPasswordControl =
|
||||
this.passwordForm.get('confirmNewPassword');
|
||||
|
||||
if (
|
||||
newPasswordControl &&
|
||||
currentPasswordControl &&
|
||||
confirmNewPasswordControl
|
||||
) {
|
||||
newPasswordControl.setValidators([
|
||||
Validators.required,
|
||||
Validators.minLength(10),
|
||||
Validators.maxLength(20),
|
||||
passwordValidator('currentPassword', PasswordValidationType.Different),
|
||||
hasUpperCaseValidator(),
|
||||
hasNumberValidator(),
|
||||
]);
|
||||
|
||||
// Apply the match validator to the confirmNewPassword control
|
||||
confirmNewPasswordControl.setValidators([
|
||||
Validators.required,
|
||||
passwordValidator('newPassword', PasswordValidationType.Match),
|
||||
]);
|
||||
|
||||
// Set up a subscription to the currentPassword field's valueChanges observable
|
||||
newPasswordControl.valueChanges.subscribe(() => {
|
||||
this.updateErrorMessage();
|
||||
confirmNewPasswordControl.updateValueAndValidity();
|
||||
});
|
||||
|
||||
confirmNewPasswordControl.valueChanges.subscribe(() => {
|
||||
this.updateErrorMessage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to query parameters
|
||||
this.route.queryParams
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(async (params) => {
|
||||
const score = params['score'];
|
||||
const uid = params['uid'];
|
||||
if (this.isUserVerified() && score && uid) {
|
||||
this.showProfile = false;
|
||||
this.scoreTableService.validateAndAddGame(score, uid);
|
||||
this.scoreTableService.setShowUserGames(true);
|
||||
} else if (score && uid) {
|
||||
this.showProfile = false;
|
||||
const uniqueId = this.pendingActionsService.saveTempQRCodeData({
|
||||
score,
|
||||
uid,
|
||||
});
|
||||
if (this.authService.getCurrentUser())
|
||||
this.pendingActionsService.addUserRefToQRCodeData(
|
||||
this.authService.getCurrentUser()?.uid!,
|
||||
uniqueId
|
||||
);
|
||||
this.router.navigate(['/score-table']);
|
||||
}
|
||||
|
||||
// Subscribing to the user profile changes to update component properties
|
||||
this.userProfileSubscription = this.user$.subscribe((userProfile) => {
|
||||
this.userProfilePhoto = userProfile?.photoUrl;
|
||||
});
|
||||
});
|
||||
|
||||
// Initializing the user profile form
|
||||
this.initForm();
|
||||
|
||||
// Subscribe to route params
|
||||
this.route.params.pipe(untilDestroyed(this)).subscribe((params) => {
|
||||
this.initUserProfile(params['id']);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to save the updated profile data
|
||||
saveProfile() {
|
||||
this.showSpinnerModule = true;
|
||||
this.authService.currentUser$.pipe(take(1)).subscribe((user) => {
|
||||
if (user) {
|
||||
// Check that the user is not null
|
||||
const profileData = this.userProfileForm.value;
|
||||
const updatedUser: User = {
|
||||
uid: user.uid,
|
||||
firmenposition: profileData.firmenposition,
|
||||
// Include other fields as needed
|
||||
};
|
||||
this.userService.updateUser(updatedUser).subscribe();
|
||||
setTimeout(() => (this.showSpinnerModule = false), 1000);
|
||||
this.notifierService.showNotification(
|
||||
'Firmenposition erfolgreich aktualisiert.',
|
||||
'OK'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle logout
|
||||
onLogout() {
|
||||
// Dismiss any open notifications
|
||||
this.notifierService.dismissNotification();
|
||||
this.authService.logout().subscribe(() => {
|
||||
this.router.navigate(['/login']);
|
||||
});
|
||||
this.unsubscribe$.next();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribing from subscriptions to prevent memory leaks
|
||||
if (this.userProfileSubscription) {
|
||||
this.userProfileSubscription.unsubscribe();
|
||||
}
|
||||
if (this.passwordValueChangesSubscription) {
|
||||
this.passwordValueChangesSubscription.unsubscribe();
|
||||
}
|
||||
this.unsubscribe$.next();
|
||||
this.unsubscribe$.complete();
|
||||
}
|
||||
|
||||
// Function to update the user's password
|
||||
updatePassword() {
|
||||
if (!this.passwordForm.valid) {
|
||||
return;
|
||||
}
|
||||
const currentUser = this.authService.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
this.notifierService.showNotification(
|
||||
'Kein Benutzer angemeldet. Bitte melde dich an.',
|
||||
'OK'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdatingPassword = true;
|
||||
const currentPassword = this.passwordForm.value.currentPassword;
|
||||
const newPassword = this.passwordForm.value.newPassword;
|
||||
|
||||
this.userService
|
||||
.updatePassword(currentPassword, newPassword)
|
||||
.pipe(takeUntil(this.unsubscribe$))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifierService.showNotification(
|
||||
'Das Passwort wurde geändert.',
|
||||
'OK'
|
||||
);
|
||||
this.passwordForm.reset();
|
||||
this.showChangePassword = false;
|
||||
this.isUpdatingPassword = false;
|
||||
},
|
||||
error: (error) => {
|
||||
this.isUpdatingPassword = false;
|
||||
if (error.code === 'auth/invalid-credential') {
|
||||
this.notifierService.showNotification(
|
||||
'Das aktuelle Passwort ist falsch!',
|
||||
'OK'
|
||||
);
|
||||
} else {
|
||||
console.error('Error updating password:', error);
|
||||
this.notifierService.showNotification(
|
||||
'Bitte versuche es in 5 Min nochmal!',
|
||||
'OK'
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isUserVerified() {
|
||||
return this.authService.getCurrentUser()?.emailVerified;
|
||||
}
|
||||
|
||||
initForm() {
|
||||
this.userProfileForm = new FormGroup({
|
||||
uid: new FormControl(null),
|
||||
name: new FormControl(''),
|
||||
email: new FormControl(''),
|
||||
firmenposition: new FormControl(''),
|
||||
wins: new FormControl(''),
|
||||
//New Password validation
|
||||
currentPassword: new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(10),
|
||||
]),
|
||||
newPassword: new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(10),
|
||||
Validators.maxLength(20),
|
||||
]),
|
||||
confirmNewPassword: new FormControl('', [
|
||||
Validators.required,
|
||||
passwordValidator('newPassword', PasswordValidationType.Match),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
updateForm(user: User | null) {
|
||||
if (user) {
|
||||
this.userProfileForm.patchValue({
|
||||
name: user?.firstName + ' ' + user?.lastName,
|
||||
...user,
|
||||
});
|
||||
this.setProfilePhoto(user.uid);
|
||||
}
|
||||
}
|
||||
|
||||
initUserProfile(uid: string) {
|
||||
this.user$.pipe(untilDestroyed(this)).subscribe(async (user) => {
|
||||
this.isProfileOwner = uid ? uid === user?.uid : true;
|
||||
if (this.isProfileOwner) {
|
||||
this.updateForm(user);
|
||||
} else {
|
||||
this.userService
|
||||
.getUserProfile(uid)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(async (user) => {
|
||||
this.updateForm(user);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setProfilePhoto(uid: string) {
|
||||
this.userService
|
||||
.getPhotoUrl(uid)
|
||||
.then((downloadURL) => {
|
||||
this.userProfilePhoto = downloadURL;
|
||||
})
|
||||
.finally(() => {
|
||||
this.dataLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
isCompanyAtos() {
|
||||
const email = this.userProfileForm.controls['email'].value;
|
||||
return email.includes('atos.net');
|
||||
}
|
||||
|
||||
updateErrorMessage() {
|
||||
const currentPasswordControl = this.userProfileForm.get('currentPassword');
|
||||
const newPasswordControl = this.passwordForm.get('newPassword');
|
||||
const confirmNewPasswordControl =
|
||||
this.passwordForm.get('confirmNewPassword');
|
||||
|
||||
this.errorMessageCurrentPassword = currentPasswordControl?.hasError(
|
||||
'required'
|
||||
)
|
||||
? 'Aktuelles Passwort eingeben.'
|
||||
: '';
|
||||
this.errorMessageNewPassword =
|
||||
this.getNewPasswordErrorMessage(newPasswordControl);
|
||||
this.errorMessageConfirmPassword = this.getConfirmPasswordErrorMessage(
|
||||
confirmNewPasswordControl
|
||||
);
|
||||
}
|
||||
|
||||
private getNewPasswordErrorMessage(control: AbstractControl | null): string {
|
||||
if (control?.hasError('required')) {
|
||||
return 'Neues Passwort eingeben.';
|
||||
} else if (control?.hasError('minlength')) {
|
||||
return 'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||
} else if (control?.hasError('maxlength')) {
|
||||
return 'Das Passwort darf maximal 20 Zeichen lang sein.';
|
||||
} else if (control?.hasError('passwordsSame')) {
|
||||
return 'Das neue Passwort muss sich vom aktuellen Passwort unterscheiden.';
|
||||
} else if (
|
||||
control?.hasError('hasNumber') &&
|
||||
control?.hasError('hasUpperCase')
|
||||
) {
|
||||
return 'Mind. 1 Großbuchstaben und 1 Zahl.';
|
||||
} else if (control?.hasError('hasUpperCase')) {
|
||||
return 'Mind. 1 Großbuchstaben.';
|
||||
} else if (control?.hasError('hasNumber')) {
|
||||
return 'Mind. 1 Zahl.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getConfirmPasswordErrorMessage(
|
||||
control: AbstractControl | null
|
||||
): string {
|
||||
if (control?.hasError('required')) {
|
||||
return 'Passwortbestätigung eingeben.';
|
||||
} else if (control?.hasError('passwordMismatch')) {
|
||||
return 'Die eingegebenen Passwörter stimmen nicht überein.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
196
src/app/user-profile/user.service.ts
Normal file
196
src/app/user-profile/user.service.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Firestore,
|
||||
doc,
|
||||
docData,
|
||||
setDoc,
|
||||
updateDoc,
|
||||
collection,
|
||||
query,
|
||||
where,
|
||||
getDocs,
|
||||
} from '@angular/fire/firestore';
|
||||
import { User } from '../auth/model/user.model';
|
||||
import { Observable, from, of, switchMap } from 'rxjs';
|
||||
import { AuthService } from '../auth/authService/auth.service';
|
||||
import {
|
||||
EmailAuthProvider,
|
||||
reauthenticateWithCredential,
|
||||
updatePassword,
|
||||
} from '@angular/fire/auth';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
/**
|
||||
* Retrieves the current user's profile.
|
||||
* @returns An observable emitting the current user's profile.
|
||||
*/
|
||||
get currentUserProfile$(): Observable<User | null> {
|
||||
return this.authService.currentUser$.pipe(
|
||||
switchMap((user) => {
|
||||
if (!user?.uid) {
|
||||
return of(null);
|
||||
}
|
||||
const ref = doc(this.firestore, 'users', user?.uid);
|
||||
return docData(ref) as Observable<User>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new UserService.
|
||||
* @param firestore - The Angular Firestore service instance.
|
||||
* @param authService - The authentication service instance.
|
||||
* @param httpClient - The Angular HttpClient service instance.
|
||||
*/
|
||||
constructor(private firestore: Firestore, private authService: AuthService) {}
|
||||
|
||||
/**
|
||||
* Adds a new user to the Firestore database.
|
||||
* @param user - The user object to be added.
|
||||
* @returns An observable indicating the success or failure of the operation.
|
||||
*/
|
||||
addUser(user: User): Observable<any> {
|
||||
// Ensure that the user.uid is not undefined or an empty string
|
||||
if (!user.uid) {
|
||||
throw new Error(
|
||||
'Invalid UID: UID is required to create a user document.'
|
||||
);
|
||||
}
|
||||
const ref = doc(this.firestore, 'users', user?.uid);
|
||||
return from(setDoc(ref, user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing user in the Firestore database.
|
||||
* @param user - The updated user object.
|
||||
* @returns An observable indicating the success or failure of the operation.
|
||||
*/
|
||||
updateUser(user: User): Observable<any> {
|
||||
const ref = doc(this.firestore, 'users', user?.uid);
|
||||
return from(updateDoc(ref, { ...user }));
|
||||
}
|
||||
|
||||
// UserService method to count the user's games and update the wins field
|
||||
updateUserWins(userId: string): Observable<any> {
|
||||
const userRef = doc(this.firestore, 'users', userId);
|
||||
const gamesRef = collection(this.firestore, 'games');
|
||||
const q = query(gamesRef, where('userRef', '==', userRef));
|
||||
|
||||
return from(getDocs(q)).pipe(
|
||||
switchMap((querySnapshot) => {
|
||||
// The size property indicates the number of documents in the QuerySnapshot
|
||||
const wins = querySnapshot.size;
|
||||
// Update the wins field in the user document
|
||||
return from(updateDoc(userRef, { wins: wins }));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updatePassword(
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Observable<any> {
|
||||
return this.authService.currentUser$.pipe(
|
||||
switchMap((user) => {
|
||||
if (!user) {
|
||||
throw new Error('No user signed in');
|
||||
}
|
||||
// Get the credential from the current password
|
||||
return from(this.getCredentialFromPassword(currentPassword)).pipe(
|
||||
switchMap((credential) => {
|
||||
// Reauthenticate user with their current password
|
||||
return from(reauthenticateWithCredential(user, credential)).pipe(
|
||||
switchMap(() => {
|
||||
// Re-authentication successful, proceed to update password
|
||||
return from(updatePassword(user, newPassword));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getCredentialFromPassword(currentPassword: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.getUserEmail().subscribe(
|
||||
(email: string | null) => {
|
||||
if (!email) {
|
||||
reject(new Error('User email is not available'));
|
||||
} else {
|
||||
const credential = EmailAuthProvider.credential(
|
||||
email,
|
||||
currentPassword
|
||||
);
|
||||
resolve(credential);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getUserEmail(): Observable<string | null> {
|
||||
return this.authService.currentUser$.pipe(
|
||||
switchMap((user) => {
|
||||
if (!user?.uid) {
|
||||
return of(null);
|
||||
}
|
||||
const ref = doc(this.firestore, 'users', user.uid);
|
||||
return docData(ref).pipe(
|
||||
switchMap((userData: any) => {
|
||||
// Assuming 'email' is a field in your Firestore document
|
||||
const email: string | null = userData.email || null;
|
||||
return of(email);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getUserProfile(uid: string): Observable<User | null> {
|
||||
return new Observable<User>((observer) => {
|
||||
const usersCollection = collection(this.firestore, 'users');
|
||||
const q = query(usersCollection, where('uid', '==', uid));
|
||||
getDocs(q)
|
||||
.then((querySnapshot) => {
|
||||
querySnapshot.forEach((doc) => {
|
||||
const userData = doc.data() as User;
|
||||
observer.next(userData);
|
||||
});
|
||||
observer.complete();
|
||||
})
|
||||
.catch((error) => {
|
||||
observer.error(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getPhotoUrl(uid: string | undefined): Promise<string | null> {
|
||||
if (!uid) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const filesQuery = query(
|
||||
collection(this.firestore, 'files'),
|
||||
where('id', '==', uid)
|
||||
);
|
||||
const querySnapshot = await getDocs(filesQuery);
|
||||
|
||||
if (!querySnapshot.empty) {
|
||||
const fileDoc = querySnapshot.docs[0];
|
||||
return fileDoc.data()['downloadURL'] as string;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting image file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.warning-card {
|
||||
margin: 1rem;
|
||||
background-color: #fff3cd;
|
||||
width: auto;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<mat-card *ngIf="!hideCard" class="warning-card">
|
||||
<mat-card-content>
|
||||
<p>
|
||||
Deine E-Mail wurde noch nicht verifiziert. Bitte überprüfen dein Postfach
|
||||
und klicke auf den Verifizierungslink.
|
||||
</p>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button (click)="resendVerificationEmail()">
|
||||
Neue Verifizierungs-E-Mail anfordern
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VerifiyWarnCardComponent } from './verify-email-warn-card.component';
|
||||
|
||||
describe('VerifiyWarnCardComponent', () => {
|
||||
let component: VerifiyWarnCardComponent;
|
||||
let fixture: ComponentFixture<VerifiyWarnCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VerifiyWarnCardComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VerifiyWarnCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||
import { AuthService } from '../../auth/authService/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-verify-email-warn-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AngularMaterialModule, MatDividerModule],
|
||||
templateUrl: './verify-email-warn-card.component.html',
|
||||
styleUrl: './verify-email-warn-card.component.css',
|
||||
})
|
||||
export class VerifiyWarnCardComponent {
|
||||
hideCard = false;
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.hideCard = this.isUserVerified();
|
||||
}
|
||||
|
||||
resendVerificationEmail() {
|
||||
this.authService.sendVerificationEmail();
|
||||
}
|
||||
|
||||
isUserVerified() {
|
||||
return !!this.authService.getCurrentUser()?.emailVerified;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user