Skip to content
Draft
Empty file removed front/src/app/app.component.scss
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ <h1 class="text-gray-900 text-2xl md:ml-10 text-primaryColor font-medium md:pt-0

<nav class="flex gap-1 rounded-md bg-action px-1 py-1 md:static absolute top-0 left-1/2 transform -translate-x-1/2 md:ml-auto md:-mr-25"
aria-label="Event filters">
<button
class="flex items-center rounded-md py-0.5 px-12 text-sm cursor-pointer transition-all"
[ngClass]="activeTab() === 'currents' ? 'bg-white text-primaryColor shadow-sm' : 'text-secondary'"
(click)="setActiveTab('currents')"
[attr.aria-selected]="activeTab() === 'currents'"
[disabled]="isLoading()">

<app-button
type="button"
[customClass]="getCurrentsTabClasses()"
[buttonHandler]="getCurrentsTabHandler()"
[disabled]="isLoading()"
[ariaLabel]="'Show current events'"
[attr.aria-selected]="activeTab() === 'currents'">
Currents
</button>
</app-button>

<button
class="flex items-center rounded-md py-0.5 px-12 text-sm cursor-pointer transition-all"
[ngClass]="activeTab() === 'passed' ? 'bg-white text-primaryColor shadow-sm' : 'text-secondary'"
(click)="setActiveTab('passed')"
[attr.aria-selected]="activeTab() === 'passed'"
[disabled]="isLoading()">
<app-button
type="button"
[customClass]="getPassedTabClasses()"
[buttonHandler]="getPassedTabHandler()"
[disabled]="isLoading()"
[ariaLabel]="'Show past events'"
[attr.aria-selected]="activeTab() === 'passed'">
Passed
</button>
</app-button>
</nav>
</header>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ import {
EventTeamField
} from '../../../feature/admin-management/components/event/event-team-card/interface/event-team-field';
import { Event } from '../../../feature/admin-management/type/event/event';
import { ButtonComponent } from '../../../shared/button/button.component';

@Component({
selector: 'app-is-login-home-page',
standalone: true,
imports: [CommonModule, EventTeamCardComponent],
imports: [CommonModule, EventTeamCardComponent, ButtonComponent],
templateUrl: './is-login-home-page.component.html',
styleUrl: './is-login-home-page.component.scss'
})
export class IsLoginHomePageComponent {
activeTab = signal<'currents' | 'passed'>('currents');
userEvents = signal<Event[]>([]);
isLoading = signal<boolean>(true);
error = signal<string | null>(null);
readonly activeTab = signal<'currents' | 'passed'>('currents');
readonly userEvents = signal<Event[]>([]);
readonly isLoading = signal<boolean>(true);
readonly error = signal<string | null>(null);

private eventService = inject(EventService);
private eventStatusService = inject(EventStatusService);
private readonly eventService = inject(EventService);
private readonly eventStatusService = inject(EventStatusService);

eventCounts = computed(() => {
readonly eventCounts = computed(() => {
let currentCount = 0;
let passedCount = 0;

Expand All @@ -44,7 +45,7 @@ export class IsLoginHomePageComponent {
return { current: currentCount, passed: passedCount };
});

private filteredAndSortedEvents = computed(() => {
private readonly filteredAndSortedEvents = computed(() => {
const filteredEvents = this.eventStatusService.filterEventsByStatus(
this.userEvents(),
this.activeTab() === 'passed'
Expand All @@ -53,12 +54,13 @@ export class IsLoginHomePageComponent {
return this.sortEventsByDate(filteredEvents);
});

displayedEvents = computed(() =>
readonly displayedEvents = computed(() =>
this.transformEventsToFields(this.filteredAndSortedEvents())
);
hasEvents = computed(() => this.displayedEvents().length > 0);

emptyStateMessage = computed(() => {
readonly hasEvents = computed(() => this.displayedEvents().length > 0);

readonly emptyStateMessage = computed(() => {
const counts = this.eventCounts();
if (this.activeTab() === 'currents') {
return counts.current === 0
Expand All @@ -71,6 +73,14 @@ export class IsLoginHomePageComponent {
}
});

readonly currentsTabClasses = computed(() =>
this.getTabClasses('currents')
);

readonly passedTabClasses = computed(() =>
this.getTabClasses('passed')
);

constructor() {
effect(() => {
if (this.userEvents().length === 0 && !this.isLoading()) {
Expand All @@ -80,6 +90,31 @@ export class IsLoginHomePageComponent {
this.loadUserEvents();
}

private getTabClasses(tab: 'currents' | 'passed'): string {
const baseClasses = 'flex items-center rounded-md py-0.5 px-12 text-sm cursor-pointer transition-all';
const activeClasses = this.activeTab() === tab
? 'bg-white text-primaryColor shadow-sm'
: 'text-secondary hover:text-white';

return `${baseClasses} ${activeClasses}`.trim();
}

getCurrentsTabClasses(): string {
return this.currentsTabClasses();
}

getPassedTabClasses(): string {
return this.passedTabClasses();
}

getCurrentsTabHandler(): () => void {
return () => this.setActiveTab('currents');
}

getPassedTabHandler(): () => void {
return () => this.setActiveTab('passed');
}

private loadUserEvents(): void {
this.isLoading.set(true);
this.error.set(null);
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { LoginFormComponent } from './login-form.component';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ButtonWithIconComponent } from "../../../shared/button-with-icon/button-with-icon.component";
import { MatDialog } from '@angular/material/dialog';
import { AuthErrorDialogComponent } from '../../../shared/auth-error-dialog/auth-error-dialog.component';
import { of } from 'rxjs';
import {ButtonComponent} from '../../../shared/button/button.component';

describe('LoginFormComponent', () => {
let component: LoginFormComponent;
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('LoginFormComponent', () => {
imports: [
FormsModule,
LoginFormComponent,
ButtonWithIconComponent
ButtonComponent
],
providers: [
{ provide: AuthService, useValue: authServiceMock },
Expand Down
Empty file.
16 changes: 16 additions & 0 deletions front/src/app/core/login/services/auth-backend.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthBackendService } from './auth-backend.service';

describe('AuthBackendService', () => {
let service: AuthBackendService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthBackendService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
147 changes: 147 additions & 0 deletions front/src/app/core/login/services/auth-backend.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Injectable, inject } from '@angular/core';
import {
Auth,
User as FirebaseUser,
} from '@angular/fire/auth';
import {HttpClient} from '@angular/common/http';
import {UserStateService} from '../../services/user-services/user-state.service';
import {User} from '../../models/user.model';
import {firstValueFrom} from 'rxjs';
import {environment} from '../../../../environments/environment.development';

@Injectable({
providedIn: 'root'
})
export class AuthBackendService {
private readonly http = inject(HttpClient);
private readonly auth = inject(Auth);
private readonly userState = inject(UserStateService);

async syncUserData(firebaseUser: FirebaseUser): Promise<void> {
try {
const userData = await this.fetchUserData(firebaseUser.uid);

if (userData) {
const mergedUser = this.mergeUserData(firebaseUser, userData);
this.userState.updateUser(mergedUser);
this.userState.saveToStorage();
}
} catch (error) {
console.error('Error syncing user data:', error);
}
}

async processUserLogin(user: FirebaseUser): Promise<void> {
const token = await user.getIdToken();

await Promise.all([
this.sendTokenToBackend(token),
this.processInvitations(user),
this.saveUserToBackend(this.createUserPayload(user))
]);

this.userState.updateUser(this.createUserPayload(user));
this.userState.saveToStorage();
}

private mergeUserData(firebaseUser: FirebaseUser, userData: User): User {
return {
uid: firebaseUser.uid,
email: userData.email || firebaseUser.email || '',
displayName: userData.displayName || firebaseUser.displayName || '',
photoURL: userData.photoURL || firebaseUser.photoURL || '',
company: userData.company || '',
city: userData.city || '',
phoneNumber: userData.phoneNumber || '',
githubLink: userData.githubLink || '',
twitterLink: userData.twitterLink || '',
blueSkyLink: userData.blueSkyLink || '',
linkedInLink: userData.linkedInLink || '',
biography: userData.biography || '',
otherLink: userData.otherLink || ''
};
}

private createUserPayload(user: FirebaseUser): Partial<User> {
return {
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL
};
}

async getIdToken(forceRefresh = true): Promise<string | null> {
try {
if (!this.auth.currentUser) return null;

const token = await this.auth.currentUser.getIdToken(forceRefresh);
await this.sendTokenToBackend(token);
return token;
} catch {
return null;
}
}

async logout(): Promise<void> {
try {
await firstValueFrom(
this.http.post(`${environment.apiUrl}/auth/logout`, {}, { withCredentials: true })
);
} catch (error) {
console.error('Backend logout error:', error);
}
}

private async fetchUserData(uid: string): Promise<User | null> {
try {
return await firstValueFrom(
this.http.get<User>(`${environment.apiUrl}/auth/user/${uid}`, { withCredentials: true })
);
} catch {
return null;
}
}

private async sendTokenToBackend(token: string): Promise<void> {
await firstValueFrom(
this.http.post(`${environment.apiUrl}/auth/login`, { idToken: token }, { withCredentials: true })
);
}

private async saveUserToBackend(user: Partial<User>): Promise<void> {
if (!user?.uid) return;

await firstValueFrom(
this.http.post(`${environment.apiUrl}/auth`, user, { withCredentials: true })
);
}

async processInvitations(user: FirebaseUser): Promise<void> {
if (!user?.email) return;

try {
await firstValueFrom(
this.http.post(`${environment.apiUrl}/public/invitations/process`, {
email: user.email.toLowerCase(),
uid: user.uid
})
);
} catch (error) {
console.error('Error processing invitations:', error);
}
}

async getCurrentUserToken(): Promise<string | null> {
try {
const currentUser = this.auth.currentUser;
if (currentUser) {
return await currentUser.getIdToken(true);
}
return null;
} catch (error) {
console.error('Error getting user token:', error);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { EmailLinkService } from './email-link.service';

describe('EmailLinkService', () => {
let service: EmailLinkService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(EmailLinkService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
39 changes: 39 additions & 0 deletions front/src/app/core/login/services/auth-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {inject, Injectable} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {AuthErrorDialogComponent} from '../../../shared/auth-error-dialog/auth-error-dialog.component';

@Injectable({
providedIn: 'root'
})
export class AuthErrorHandlerService {
private readonly dialog = inject(MatDialog);

handleProviderError(error: any): null {
if (error.code === 'auth/account-exists-with-different-credential') {
this.showAuthErrorDialog(error.customData.email);
}
return null;
}

showAuthErrorDialog(email: string): void {
this.dialog.open(AuthErrorDialogComponent, {
width: '400px',
data: {
title: 'Authentication Error',
email,
message: `The email address "${email}" is already associated with another sign-in method.`
}
});
}

showSuccessDialog(title: string, message: string): void {
this.dialog.open(AuthErrorDialogComponent, {
width: '400px',
data: { title, message }
});
}

openDialog(component: any, config: any) {
return this.dialog.open(component, config);
}
}
Loading