Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/mail/mail.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { MailService } from './mail.service';

@Module({
providers: [MailService],
exports: [MailService], // ✅ VERY IMPORTANT
exports: [MailService],
})
export class MailModule {}
6 changes: 5 additions & 1 deletion frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { RegisterComponent } from './auth/register/register.component';
import { LoginComponent } from './auth/login/login.component';
import { authGuard } from './core/guards/auth.guard';
import { rolesGuard } from './core/guards/roles.guard';
import { ForgotPasswordComponent } from './auth/forgot-password/forgot-password';
import { ResetPasswordComponent } from './auth/reset-password/reset-password';

export const routes: Routes = [
// Public routes
{ path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent },
{ path: 'forgot-password', component: ForgotPasswordComponent },

{ path: 'reset-password', component: ResetPasswordComponent },
// Protected routes (canActivate: [authGuard] applied — add components as they are built)
// Any logged-in user
// { path: 'events', component: EventListComponent, canActivate: [authGuard] },
Expand All @@ -28,4 +32,4 @@ export const routes: Routes = [
];

// Re-export guards so other modules can import from one place
export { authGuard, rolesGuard };
export { authGuard, rolesGuard };
18 changes: 15 additions & 3 deletions frontend/src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface LoginResponse {
access_token: string;
}

interface ResetPasswordDto {
token: string;
password: string;
}
@Injectable({
providedIn: 'root',
})
Expand All @@ -35,10 +39,12 @@ export class AuthService {
tap((res) => {
// Save JWT in localStorage
localStorage.setItem('token', res.access_token);
})
}),
);
}

reset(dto: ResetPasswordDto): Observable<any> {
return this.http.post(`${this.apiUrl}/reset-password`, dto);
}
logout(): void {
localStorage.removeItem('token');
}
Expand All @@ -51,6 +57,12 @@ export class AuthService {
return !!this.getToken();
}

forgotPassword(email: string) {
return this.http.post(`${this.apiUrl}/forgot-password`, {
email: email,
});
}

/**
* Decodes the JWT payload and returns the user's role.
* Returns null if no token or token is malformed.
Expand Down Expand Up @@ -79,4 +91,4 @@ export class AuthService {
return null;
}
}
}
}
21 changes: 21 additions & 0 deletions frontend/src/app/auth/forgot-password/forgot-password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="forgot-container">
<h2>Forgot Password</h2>

<form [formGroup]="forgotForm" (ngSubmit)="onSubmit()">
<label>Email</label>
<input type="email" formControlName="email" />
@if (forgotForm.get('email')?.invalid && forgotForm.get('email')?.touched) {
<div class="error">Enter a valid email</div>
}

<button type="submit" [disabled]="loading">
{{ loading ? 'Sent' : 'Send Reset Link' }}
</button>
</form>
@if (message) {
<p class="success">{{ message }}</p>
}
@if (error) {
<p class="error">{{ error }}</p>
}
</div>
73 changes: 73 additions & 0 deletions frontend/src/app/auth/forgot-password/forgot-password.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.forgot-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #ccc;
border-radius: 10px;
background: #f9f9f9;

h2 {
text-align: center;
margin-bottom: 1rem;
}

form {
display: flex;
flex-direction: column;

label {
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
font-weight: bold;

input {
padding: 0.5rem;
margin-top: 0.25rem;
border-radius: 5px;
border: 1px solid #ccc;
}
}

button {
margin-top: 1rem;
padding: 0.5rem;
border: none;
background-color: #007bff;
color: white;
border-radius: 5px;
cursor: pointer;

&:disabled {
background-color: #999;
cursor: not-allowed;
}
}

p {
margin-top: 1rem;
text-align: center;

a {
color: #007bff;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}
}

.message {
color: green;
text-align: center;
margin-top: 0.5rem;
}

.error {
color: red;
font-size: 0.9rem;
text-align: center;
}
}
22 changes: 22 additions & 0 deletions frontend/src/app/auth/forgot-password/forgot-password.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ForgotPasswordComponent } from './forgot-password';

describe('ForgotPasswordComponent', () => {
let component: ForgotPasswordComponent;
let fixture: ComponentFixture<ForgotPasswordComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ForgotPasswordComponent],
}).compileComponents();

fixture = TestBed.createComponent(ForgotPasswordComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
58 changes: 58 additions & 0 deletions frontend/src/app/auth/forgot-password/forgot-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Component, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../auth.service';

@Component({
selector: 'app-forgot-password',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './forgot-password.html',
styleUrls: ['./forgot-password.scss'],
})
export class ForgotPasswordComponent {
forgotForm: FormGroup;
message: string = '';
error: string = '';
loading: boolean = false;
constructor(
private fb: FormBuilder,
private auth: AuthService,
private cdr: ChangeDetectorRef,
) {
this.forgotForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
}

onSubmit() {
if (this.loading) return; // prevents second click

if (this.forgotForm.invalid) {
this.forgotForm.markAllAsTouched();
return;
}

this.loading = true; // disable button
this.message = '';
this.error = '';

const email = this.forgotForm.value.email;

this.auth.forgotPassword(email).subscribe({
next: () => {
this.message = 'Password reset email sent. Check your inbox.';
this.cdr.detectChanges();
setTimeout(() => {
this.loading = true;
}, 1);
},

error: (err) => {
this.error = err.error?.message ?? 'Failed to send reset email';
this.cdr.detectChanges();
this.loading = false;
},
});
}
}
26 changes: 17 additions & 9 deletions frontend/src/app/auth/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@ <h2>Login</h2>
Email
<input formControlName="email" />
</label>
<div *ngIf="loginForm.get('email')?.invalid && loginForm.get('email')?.touched" class="error">
Enter a valid email
</div>
@if (loginForm.get('email')?.invalid && loginForm.get('email')?.touched) {
<div class="error">Enter a valid email</div>
}

<label>
Password
<input type="password" formControlName="password" />
</label>
<div *ngIf="loginForm.get('password')?.invalid && loginForm.get('password')?.touched" class="error">
Password must be at least 6 characters
</div>
@if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {
<div class="error">Password must be at least 8 characters</div>
}

<button type="submit" [disabled]="loginForm.invalid">Login</button>

<p>
Don't have an account?
<a [routerLink]="['/register']">Go to Registration</a>
</p>
<p>
Forgot Password?
<a [routerLink]="['/forgot-password']">Reset</a>
</p>
</form>

<p class="message" *ngIf="message">{{ message }}</p>
<p class="error" *ngIf="error">{{ error }}</p>
</div>
@if (message) {
<p class="message">{{ message }}</p>
}
@if (error) {
<p class="error">{{ error }}</p>
}
</div>
17 changes: 10 additions & 7 deletions frontend/src/app/auth/login/login.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router, RouterModule } from '@angular/router';
Expand All @@ -9,7 +9,7 @@ import { AuthService } from '../auth.service';
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterModule],
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
styleUrls: ['./login.component.scss'],
})
export class LoginComponent {
loginForm: FormGroup;
Expand All @@ -20,21 +20,24 @@ export class LoginComponent {
private fb: FormBuilder,
private auth: AuthService,
private router: Router,
private cdr: ChangeDetectorRef,
) {
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
password: ['', [Validators.required, Validators.minLength(8)]],
});
}

onSubmit() {
this.loginForm.markAllAsTouched();

if (this.loginForm.valid) {
this.auth.login(this.loginForm.value).subscribe({
next: () => {
this.message = 'Login successful! Redirecting...';
this.error = '';
this.router.navigate(['/events']);
this.cdr.detectChanges();
setTimeout(() => {
this.router.navigate(['/events']);
}, 1000);
},
error: (err) => {
this.error = err.error?.message ?? 'Invalid email or password';
Expand All @@ -45,4 +48,4 @@ export class LoginComponent {
this.error = 'Please enter valid email and password.';
}
}
}
}
Loading