227 lines
8.8 KiB
TypeScript
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 };
|
|
}
|
|
}
|