first commit

This commit is contained in:
2025-07-22 22:10:30 +02:00
commit 9a1ffc51d8
130 changed files with 20161 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -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.');
}
}
}

View File

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

View 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;
}

View 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>

View 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 '';
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,5 @@
.warning-card {
margin: 1rem;
background-color: #fff3cd;
width: auto;
}

View File

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

View File

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

View File

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