Authentication with firebase, Landing Page 1.Teil

This commit is contained in:
2025-01-18 23:00:45 +00:00
parent d17578d123
commit 83035a6b82
25 changed files with 5612 additions and 3008 deletions

View File

@@ -1,7 +1,9 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { Auth } from '@angular/fire/auth';
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { DeckListComponent } from './deck-list.component';
import { ClickOutsideDirective } from './service/click-outside.directive';
@Component({
selector: 'app-root',
template: `
@@ -25,22 +27,101 @@ import { DeckListComponent } from './deck-list.component';
</div>
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-center mb-8">Vocabulary Training</h1>
<div class="flex justify-center items-center mb-8">
<h1 class="text-3xl font-bold mx-auto">Vocabulary Training</h1>
<div class="relative" appClickOutside (clickOutside)="showDropdown = false">
<img [src]="photoURL" alt="User Photo" class="w-10 h-10 rounded-full cursor-pointer" (click)="toggleDropdown()" />
<div *ngIf="showDropdown" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg">
<button (click)="logout()" class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">Abmelden</button>
</div>
</div>
</div>
<app-deck-list></app-deck-list>
</div>
`,
standalone: true,
imports: [CommonModule, DeckListComponent],
styles: `
img {
border: 2px solid #fff;
transition: transform 0.2s;
}
img:hover {
transform: scale(1.1);
}
/* Stile für das Dropdown-Menü */
.dropdown {
display: none;
position: absolute;
right: 0;
margin-top: 0.5rem;
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.dropdown button {
width: 100%;
text-align: left;
padding: 0.5rem 1rem;
color: #4a5568;
}
.dropdown button:hover {
background-color: #f7fafc;
}
`,
imports: [CommonModule, DeckListComponent, ClickOutsideDirective],
})
export class AppComponent {
isLoggedIn = false; // Zustand für den Login-Status
isLoggedIn = false;
private auth: Auth = inject(Auth);
showDropdown = false;
photoURL: string = 'https://via.placeholder.com/40';
// user: User | null = null;
// Mock-Funktion für Google Login
loginWithGoogle() {
// Hier würde die eigentliche Google Login-Logik stehen
// Zum Beispiel mit Angular Fire oder einer anderen Bibliothek
// Für dieses Beispiel simulieren wir den Login:
this.isLoggedIn = true;
console.log('Logged in with Google');
ngOnInit() {
// Überprüfen des Login-Status beim Start der Anwendung
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
this.photoURL = localStorage.getItem('photoURL');
this.showDropdown = false;
if (isLoggedIn && accessToken && refreshToken) {
this.isLoggedIn = true;
}
}
async loginWithGoogle() {
const provider = new GoogleAuthProvider();
try {
const result = await signInWithPopup(this.auth, provider);
this.isLoggedIn = true;
this.photoURL = result.user.photoURL;
// Speichern des Login-Status und Tokens im Local Storage
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('accessToken', await result.user.getIdToken());
localStorage.setItem('refreshToken', result.user.refreshToken);
localStorage.setItem('photoURL', result.user.photoURL);
console.log('Logged in with Google', result.user);
} catch (error) {
console.error('Google Login failed', error);
}
}
logout() {
this.isLoggedIn = false;
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// Optional: Firebase-Logout durchführen
this.auth.signOut();
}
toggleDropdown() {
this.showDropdown = !this.showDropdown;
}
}

View File

@@ -1,9 +1,25 @@
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { environment } from './environments/environment';
import { authInterceptor } from './service/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),provideHttpClient()]
providers: [
// {
// provide: HTTP_INTERCEPTORS,
// useClass: AuthInterceptor,
// multi: true,
// },
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor]), // Hier wird der Interceptor registriert
),
],
};

View File

@@ -7,7 +7,6 @@ import { Box, Deck, DeckImage, DeckService, OcrResult } from './deck.service';
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
import { TrainingComponent } from './training/training.component';
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component';
@Component({
selector: 'app-deck-list',
@@ -26,11 +25,9 @@ import { UploadImageModalComponent } from './upload-image-modal/upload-image-mod
imports: [
CommonModule,
CreateDeckModalComponent,
UploadImageModalComponent,
TrainingComponent,
EditImageModalComponent,
MoveImageModalComponent, // Adding the new component
UploadImageModalComponent,
],
})
export class DeckListComponent implements OnInit {
@@ -43,10 +40,7 @@ export class DeckListComponent implements OnInit {
@ViewChild(CreateDeckModalComponent)
createDeckModal!: CreateDeckModalComponent;
@ViewChild(UploadImageModalComponent)
uploadImageModal!: UploadImageModalComponent;
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
@ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent;
@ViewChild('imageFile') imageFileElement!: ElementRef;
imageData: {
@@ -260,12 +254,6 @@ export class DeckListComponent implements OnInit {
this.createDeckModal.open();
}
// Method to open the upload image modal
openUploadImageModal(deckName: string): void {
this.uploadImageModal.deckName = deckName;
this.uploadImageModal.open();
}
// Method to check if a deck is expanded
isDeckExpanded(deckName: string): boolean {
return this.expandedDecks.has(deckName);
@@ -293,11 +281,6 @@ export class DeckListComponent implements OnInit {
sessionStorage.setItem('expandedDecks', JSON.stringify(expandedArray));
}
// Method to open the upload modal
openUploadModal(): void {
this.uploadImageModal.open();
}
// Handler for the imageUploaded event
onImageUploaded(imageData: any): void {
this.imageData = imageData;

View File

@@ -0,0 +1,12 @@
export const environment = {
production: true,
firebase: {
apiKey: 'AIzaSyBBH7mGJtwY-6_x0kCmyWCGe6JCesRS49k',
authDomain: 'haiki-452bd.firebaseapp.com',
projectId: 'haiki-452bd',
storageBucket: 'haiki-452bd.firebasestorage.app',
messagingSenderId: '263288723576',
appId: '1:263288723576:web:2bc87146ef52d276f5358d',
measurementId: 'G-C1C3N16KB3',
},
};

View File

@@ -0,0 +1,12 @@
export const environment = {
production: false,
firebase: {
apiKey: 'AIzaSyBBH7mGJtwY-6_x0kCmyWCGe6JCesRS49k',
authDomain: 'haiki-452bd.firebaseapp.com',
projectId: 'haiki-452bd',
storageBucket: 'haiki-452bd.firebasestorage.app',
messagingSenderId: '263288723576',
appId: '1:263288723576:web:2bc87146ef52d276f5358d',
measurementId: 'G-C1C3N16KB3',
},
};

View File

@@ -0,0 +1,73 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Auth } from '@angular/fire/auth';
import { from } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(Auth); // Injizieren Sie den Auth-Dienst
const token = localStorage.getItem('accessToken');
if (token) {
const decodedToken = decodeToken(token);
const expirationTime = decodedToken.exp * 1000; // Umwandeln in Millisekunden
const currentTime = Date.now();
if (currentTime > expirationTime) {
// Token ist abgelaufen, erneuern Sie es
return from(refreshToken(auth)).pipe(
switchMap(newToken => {
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${newToken}`,
},
});
return next(clonedReq);
}),
catchError(error => {
console.error('Failed to refresh token', error);
return next(req); // Ohne Token fortfahren
}),
);
} else {
// Token ist gültig
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
return next(clonedReq);
}
} else {
return next(req);
}
};
async function refreshToken(auth: Auth): Promise<string> {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(async user => {
if (user) {
try {
const newToken = await user.getIdToken(true); // Token erneuern
localStorage.setItem('accessToken', newToken);
resolve(newToken);
} catch (error) {
console.error('Failed to refresh token', error);
reject(error);
}
} else {
reject(new Error('No authenticated user found'));
}
unsubscribe(); // Abonnement beenden
});
});
}
function decodeToken(token: string): any {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch (e) {
console.error('Error decoding token', e);
return null;
}
}

View File

@@ -0,0 +1,19 @@
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({
selector: '[appClickOutside]',
standalone: true,
})
export class ClickOutsideDirective {
@Output() clickOutside = new EventEmitter<void>();
constructor(private elementRef: ElementRef) {}
@HostListener('document:click', ['$event.target'])
onClick(target: HTMLElement) {
const clickedInside = this.elementRef.nativeElement.contains(target);
if (!clickedInside) {
this.clickOutside.emit();
}
}
}

View File

@@ -1,43 +0,0 @@
<div #uploadImageModal id="uploadImageModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
<div class="relative h-full contents">
<div class="relative bg-white rounded-lg shadow">
<button type="button" class="absolute top-3 right-2.5 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" (click)="closeModal()">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<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>
<span class="sr-only">Close</span>
</button>
<div class="p-6 relative">
<h3 class="mb-4 text-xl font-medium text-gray-900">Add Image to Deck</h3>
<!-- Upload form -->
<div class="mb-4">
<!-- <label for="imageFile" class="block text-sm font-medium text-gray-700">Upload Image</label> -->
<!-- <input #imageFile type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> -->
<div class="relative">
<!-- Benutzerdefinierte Schaltfläche und Text -->
<label for="imageFile" class="block w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 cursor-pointer text-center">
Choose File
</label>
<!-- Das eigentliche Datei-Input-Feld -->
<input #imageFile type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="hidden" />
<!-- Anzeige des ausgewählten Dateinamens -->
<!-- <span id="fileName" class="block mt-2 text-sm text-gray-700 text-center">No file chosen</span> -->
</div>
</div>
<!-- Status display -->
<div *ngIf="processingStatus" class="mt-4">
<p class="text-sm text-gray-700">{{ processingStatus }}</p>
</div>
<!-- Loading overlay -->
<div *ngIf="loading" class="absolute inset-0 bg-gray-800 bg-opacity-50 flex items-center justify-center z-10">
<div class="bg-white p-4 rounded shadow">
<p class="text-sm text-gray-700">Processing in progress...</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,126 +0,0 @@
import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { Box, DeckImage, DeckService, OcrResult } from '../deck.service';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Modal } from 'flowbite';
@Component({
selector: 'app-upload-image-modal',
templateUrl: './upload-image-modal.component.html',
standalone: true,
styles:`
label {
transition: background-color 0.3s ease;
}
label:hover {
background-color: #2563eb;
}
`,
imports: [CommonModule]
})
export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
@Input() deckName: string = '';
@Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage: DeckImage }>();
@ViewChild('uploadImageModal') modalElement!: ElementRef;
@ViewChild('imageFile') imageFileElement!: ElementRef;
imageFile: File | null = null;
processingStatus: string = '';
loading: boolean = false;
modal: any;
constructor(private deckService: DeckService, private http: HttpClient) { }
ngAfterViewInit(): void {
this.modal = new Modal(this.modalElement.nativeElement);
}
ngOnDestroy(): void {
// Modal is automatically managed by Flowbite
}
open(): void {
this.resetState();
this.modal.show();
}
closeModal(): void {
this.modal.hide();
}
resetState(): void {
this.imageFile = null;
this.processingStatus = '';
this.loading = false;
}
onFileChange(event: any): void {
const file: File = event.target.files[0];
if (!file) return;
const fileNameElement = document.getElementById('fileName');
if (fileNameElement) {
fileNameElement.textContent = file.name;
}
this.imageFile = file;
this.processingStatus = 'Processing in progress...';
this.loading = true;
const reader = new FileReader();
reader.onload = async (e) => {
const imageSrc = e.target?.result;
// Image as Base64 string without prefix (data:image/...)
const imageBase64 = (imageSrc as string).split(',')[1];
try {
const response = await this.http.post<any>('/api/ocr', { image: imageBase64 }).toPromise();
if (!response || !response.results) {
this.processingStatus = 'Invalid response from OCR service';
this.loading = false;
return;
}
this.processingStatus = 'Processing complete';
this.loading = false;
// Emit event with image data and OCR results
const imageName = this.imageFile?.name ?? '';
const imageId = response.results.length > 0 ? response.results[0].name : null;
const boxes: Box[] = [];
response.results.forEach((result: OcrResult) => {
const box = result.box;
const xs = box.map((point: number[]) => point[0]);
const ys = box.map((point: number[]) => point[1]);
const xMin = Math.min(...xs);
const xMax = Math.max(...xs);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
boxes.push({ x1: xMin, x2: xMax, y1: yMin, y2: yMax });
});
const deckImage: DeckImage = { name: imageName, id: imageId, boxes };
this.imageUploaded.emit({ imageSrc, deckImage });
this.resetFileInput();
// Close the upload modal
this.closeModal();
} catch (error) {
console.error('Error with OCR service:', error);
this.processingStatus = 'Error with OCR service';
this.loading = false;
}
};
reader.readAsDataURL(file);
}
/**
* Resets the file input field so the same file can be selected again.
*/
resetFileInput(): void {
if (this.imageFileElement && this.imageFileElement.nativeElement) {
this.imageFileElement.nativeElement.value = '';
}
}
}