From d2e5e6acac952114046bad608e024473d844ac31 Mon Sep 17 00:00:00 2001 From: Avnish Raut Date: Tue, 10 Mar 2026 19:09:53 +0100 Subject: [PATCH 1/3] Adding UI for reset password and chaning *ngif to if for auth --- backend/src/mail/mail.module.ts | 2 +- frontend/src/app/app.routes.ts | 6 +- frontend/src/app/auth/auth.service.ts | 18 ++++- .../auth/forgot-password/forgot-password.html | 21 ++++++ .../auth/forgot-password/forgot-password.scss | 73 +++++++++++++++++++ .../forgot-password/forgot-password.spec.ts | 22 ++++++ .../auth/forgot-password/forgot-password.ts | 56 ++++++++++++++ .../src/app/auth/login/login.component.html | 26 ++++--- .../src/app/auth/login/login.component.ts | 6 +- .../app/auth/register/register.component.html | 29 ++++---- .../auth/reset-password/reset-password.html | 27 +++++++ .../auth/reset-password/reset-password.scss | 73 +++++++++++++++++++ .../reset-password/reset-password.spec.ts | 22 ++++++ .../app/auth/reset-password/reset-password.ts | 63 ++++++++++++++++ 14 files changed, 414 insertions(+), 30 deletions(-) create mode 100644 frontend/src/app/auth/forgot-password/forgot-password.html create mode 100644 frontend/src/app/auth/forgot-password/forgot-password.scss create mode 100644 frontend/src/app/auth/forgot-password/forgot-password.spec.ts create mode 100644 frontend/src/app/auth/forgot-password/forgot-password.ts create mode 100644 frontend/src/app/auth/reset-password/reset-password.html create mode 100644 frontend/src/app/auth/reset-password/reset-password.scss create mode 100644 frontend/src/app/auth/reset-password/reset-password.spec.ts create mode 100644 frontend/src/app/auth/reset-password/reset-password.ts diff --git a/backend/src/mail/mail.module.ts b/backend/src/mail/mail.module.ts index 53bc297..1ea1d72 100644 --- a/backend/src/mail/mail.module.ts +++ b/backend/src/mail/mail.module.ts @@ -3,6 +3,6 @@ import { MailService } from './mail.service'; @Module({ providers: [MailService], - exports: [MailService], // ✅ VERY IMPORTANT + exports: [MailService], }) export class MailModule {} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index a36dcf0..a5daf94 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -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] }, @@ -28,4 +32,4 @@ export const routes: Routes = [ ]; // Re-export guards so other modules can import from one place -export { authGuard, rolesGuard }; \ No newline at end of file +export { authGuard, rolesGuard }; diff --git a/frontend/src/app/auth/auth.service.ts b/frontend/src/app/auth/auth.service.ts index 3ab8140..a88addc 100644 --- a/frontend/src/app/auth/auth.service.ts +++ b/frontend/src/app/auth/auth.service.ts @@ -18,6 +18,10 @@ interface LoginResponse { access_token: string; } +interface ResetPasswordDto { + token: string; + password: string; +} @Injectable({ providedIn: 'root', }) @@ -35,10 +39,12 @@ export class AuthService { tap((res) => { // Save JWT in localStorage localStorage.setItem('token', res.access_token); - }) + }), ); } - + reset(dto: ResetPasswordDto): Observable { + return this.http.post(`${this.apiUrl}/reset-password`, dto); + } logout(): void { localStorage.removeItem('token'); } @@ -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. @@ -79,4 +91,4 @@ export class AuthService { return null; } } -} \ No newline at end of file +} diff --git a/frontend/src/app/auth/forgot-password/forgot-password.html b/frontend/src/app/auth/forgot-password/forgot-password.html new file mode 100644 index 0000000..14c3793 --- /dev/null +++ b/frontend/src/app/auth/forgot-password/forgot-password.html @@ -0,0 +1,21 @@ +
+

Forgot Password

+ +
+ + + @if (forgotForm.get('email')?.invalid && forgotForm.get('email')?.touched) { +
Enter a valid email
+ } + + +
+ @if (message) { +

{{ message }}

+ } + @if (error) { +

{{ error }}

+ } +
diff --git a/frontend/src/app/auth/forgot-password/forgot-password.scss b/frontend/src/app/auth/forgot-password/forgot-password.scss new file mode 100644 index 0000000..348f65f --- /dev/null +++ b/frontend/src/app/auth/forgot-password/forgot-password.scss @@ -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; + } +} diff --git a/frontend/src/app/auth/forgot-password/forgot-password.spec.ts b/frontend/src/app/auth/forgot-password/forgot-password.spec.ts new file mode 100644 index 0000000..cfdf701 --- /dev/null +++ b/frontend/src/app/auth/forgot-password/forgot-password.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ForgotPasswordComponent } from './forgot-password'; + +describe('ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForgotPasswordComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/auth/forgot-password/forgot-password.ts b/frontend/src/app/auth/forgot-password/forgot-password.ts new file mode 100644 index 0000000..dd0186e --- /dev/null +++ b/frontend/src/app/auth/forgot-password/forgot-password.ts @@ -0,0 +1,56 @@ +import { Component } 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, + ) { + 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.'; + + setTimeout(() => { + this.loading = true; + }, 1); + }, + + error: (err) => { + this.error = err.error?.message ?? 'Failed to send reset email'; + this.loading = false; + }, + }); + } +} diff --git a/frontend/src/app/auth/login/login.component.html b/frontend/src/app/auth/login/login.component.html index f087187..6a1383e 100644 --- a/frontend/src/app/auth/login/login.component.html +++ b/frontend/src/app/auth/login/login.component.html @@ -6,17 +6,17 @@

Login

Email -
- Enter a valid email -
+ @if (loginForm.get('email')?.invalid && loginForm.get('email')?.touched) { +
Enter a valid email
+ } -
- Password must be at least 6 characters -
+ @if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) { +
Password must be at least 8 characters
+ } @@ -24,8 +24,16 @@

Login

Don't have an account? Go to Registration

+

+ Forgot Password? + Reset +

-

{{ message }}

-

{{ error }}

- \ No newline at end of file + @if (message) { +

{{ message }}

+ } + @if (error) { +

{{ error }}

+ } + diff --git a/frontend/src/app/auth/login/login.component.ts b/frontend/src/app/auth/login/login.component.ts index c1e91f9..edea58c 100644 --- a/frontend/src/app/auth/login/login.component.ts +++ b/frontend/src/app/auth/login/login.component.ts @@ -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; @@ -23,7 +23,7 @@ export class LoginComponent { ) { this.loginForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], - password: ['', [Validators.required, Validators.minLength(6)]] + password: ['', [Validators.required, Validators.minLength(8)]], }); } @@ -45,4 +45,4 @@ export class LoginComponent { this.error = 'Please enter valid email and password.'; } } -} \ No newline at end of file +} diff --git a/frontend/src/app/auth/register/register.component.html b/frontend/src/app/auth/register/register.component.html index ee052ef..29ecca2 100644 --- a/frontend/src/app/auth/register/register.component.html +++ b/frontend/src/app/auth/register/register.component.html @@ -6,25 +6,24 @@

Register

Username -
- Username is required -
+ @if (registerForm.get('username')?.invalid && registerForm.get('username')?.touched) { +
Username is required
+ } -
- Enter a valid email -
- + @if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) { +
Enter a valid email
+ } -
- Password must be at least 6 characters -
+ @if (registerForm.get('password')?.invalid && registerForm.get('password')?.touched) { +
Password must be at least 8 characters
+ } - @if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) { + @if (registerForm.get('email')?.invalid && registerForm.get('email')?.dirty) {
Enter a valid email
} - @if (registerForm.get('email')?.invalid && registerForm.get('email')?.dirty) { + @if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) {
Enter a valid email
}