Files
indie/frontend/src/app/auth/verify-email/verify-email.component.ts
2026-07-08 22:55:22 +05:45

227 lines
8.8 KiB
TypeScript

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { NgIf } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { AuthService } from '../../core/services/auth.service';
type PageState = 'check-email' | 'verifying' | 'verified' | 'set-password' | 'error';
@Component({
selector: 'app-verify-email',
standalone: true,
imports: [
RouterLink, ReactiveFormsModule, NgIf,
MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule,
MatProgressSpinnerModule,
],
template: `
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<mat-card class="w-full max-w-md p-8">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-indigo-600">Indie</h1>
</div>
<div *ngIf="state === 'check-email'" class="text-center">
<div class="text-5xl mb-4">📧</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">Check your email</h2>
<p class="text-gray-500 mb-6">
We sent a verification link to <strong>{{ email }}</strong>
</p>
<p class="text-sm text-gray-400 mb-6">The link expires in 24 hours.</p>
<div *ngIf="resendMessage" class="text-green-600 text-sm mb-4">{{ resendMessage }}</div>
<div *ngIf="resendError" class="text-red-600 text-sm mb-4">{{ resendError }}</div>
<button mat-stroked-button (click)="resend()" [disabled]="resending" class="w-full mb-3">
<mat-spinner *ngIf="resending" diameter="16" class="inline-block mr-2"></mat-spinner>
{{ resending ? 'Sending...' : 'Resend verification email' }}
</button>
<a routerLink="/login" class="block text-sm text-indigo-600 font-medium hover:underline">Back to login</a>
</div>
<div *ngIf="state === 'verifying'" class="text-center">
<mat-spinner diameter="40" class="mx-auto mb-4"></mat-spinner>
<p class="text-gray-500">Verifying your email...</p>
</div>
<div *ngIf="state === 'verified'" class="text-center">
<div class="text-5xl mb-4">✅</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">Email verified!</h2>
<p class="text-gray-500 mb-6">Your email has been verified. Now set your password to activate your account.</p>
<button mat-flat-button color="primary" (click)="state = 'set-password'" class="w-full">
Set Your Password
</button>
</div>
<div *ngIf="state === 'set-password'">
<h2 class="text-xl font-semibold text-gray-800 mb-4 text-center">Set your password</h2>
<form [formGroup]="passwordForm" (ngSubmit)="onSetPassword()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>New Password</mat-label>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="passwordForm.get('password')?.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="passwordForm.get('password')?.hasError('minlength')">At least 8 characters</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Confirm Password</mat-label>
<input matInput type="password" formControlName="confirmPassword" />
<mat-error *ngIf="passwordForm.get('confirmPassword')?.hasError('required')">Please confirm your password</mat-error>
<mat-error *ngIf="passwordForm.hasError('mismatch')">Passwords do not match</mat-error>
</mat-form-field>
<div *ngIf="passwordError" class="text-red-600 text-sm">{{ passwordError }}</div>
<button mat-flat-button color="primary" type="submit" [disabled]="passwordLoading || passwordForm.invalid" class="w-full py-2">
<mat-spinner *ngIf="passwordLoading" diameter="20" class="inline-block mr-2"></mat-spinner>
{{ passwordLoading ? 'Setting password...' : 'Activate Account' }}
</button>
</form>
</div>
<div *ngIf="state === 'error'" class="text-center">
<div class="text-5xl mb-4">❌</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">{{ errorTitle }}</h2>
<p class="text-gray-500 mb-6">{{ errorMessage }}</p>
<button *ngIf="showResend" mat-stroked-button (click)="resend()" [disabled]="resending" class="w-full mb-3">
{{ resending ? 'Sending...' : 'Resend verification email' }}
</button>
<a *ngIf="!showResend" routerLink="/register" class="block text-sm text-indigo-600 font-medium hover:underline mb-2">Create a new account</a>
<a routerLink="/login" class="block text-sm text-indigo-600 font-medium hover:underline">Back to login</a>
</div>
</mat-card>
</div>
`,
})
export class VerifyEmailComponent implements OnInit {
state: PageState = 'check-email';
email = '';
errorTitle = '';
errorMessage = '';
showResend = false;
resendMessage = '';
resendError = '';
resending = false;
private setupToken = '';
passwordLoading = false;
passwordError = '';
passwordForm = this.fb.group({
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
}, { validators: this.passwordsMatch });
constructor(
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
private fb: FormBuilder,
) {}
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
const token = params['token'];
const email = params['email'];
if (token) {
this.verify(token);
} else if (email) {
this.email = email;
this.state = 'check-email';
} else {
this.state = 'check-email';
}
});
}
private verify(token: string): void {
this.state = 'verifying';
this.authService.verifyEmail(token).subscribe({
next: (res) => {
if (res.setupToken) {
this.setupToken = res.setupToken;
this.state = 'verified';
} else {
this.state = 'verified';
}
},
error: (err) => {
const msg = err.error?.detail || err.error?.message || 'Verification failed';
if (msg.toLowerCase().includes('expired')) {
this.errorTitle = 'Link expired';
this.errorMessage = 'This verification link has expired. Request a new one.';
this.showResend = true;
} else if (msg.toLowerCase().includes('already been used')) {
this.errorTitle = 'Already verified';
this.errorMessage = 'This link has already been used. Your email may already be verified. Try logging in.';
this.showResend = false;
} else {
this.errorTitle = 'Verification failed';
this.errorMessage = msg;
this.showResend = true;
}
this.state = 'error';
},
});
}
resend(): void {
if (!this.email) {
const token = this.route.snapshot.queryParams['token'];
this.authService.verifyEmail(token).subscribe({
next: () => {},
error: (err) => {
this.email = err.error?.email || '';
},
});
return;
}
this.resending = true;
this.resendMessage = '';
this.resendError = '';
this.authService.resendVerification(this.email).subscribe({
next: () => {
this.resendMessage = 'Verification email resent. Please check your inbox.';
this.resending = false;
this.state = 'check-email';
},
error: () => {
this.resendError = 'Failed to resend. Please try again later.';
this.resending = false;
},
});
}
onSetPassword(): void {
if (this.passwordForm.invalid) return;
this.passwordLoading = true;
this.passwordError = '';
this.authService.setPassword(this.setupToken, this.passwordForm.value.password!).subscribe({
next: () => {
this.router.navigate(['/login'], { queryParams: { verified: 'true' } });
},
error: (err) => {
this.passwordError = err.error?.detail || err.error?.message || 'Failed to set password';
this.passwordLoading = false;
},
});
}
private passwordsMatch(group: any): { mismatch: boolean } | null {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return password === confirm ? null : { mismatch: true };
}
}