optimize account creation and verification

This commit is contained in:
Krrish Ghimire
2026-07-10 15:11:20 +05:45
parent bbd6e6d007
commit a3f9cef883
14 changed files with 135 additions and 60 deletions

View File

@@ -2,7 +2,6 @@ package com.krrishg.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@@ -11,11 +10,4 @@ public class RegisterRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, max = 100, message = "Password must be between 8 and 100 characters")
private String password;
@NotBlank(message = "Name is required")
private String name;
}

View File

@@ -16,4 +16,7 @@ public class SetPasswordRequest {
@NotBlank
@Size(min = 8, max = 100)
private String password;
@NotBlank(message = "Name is required")
private String name;
}

View File

@@ -29,7 +29,7 @@ public class User {
@Column(name = "password")
private String password;
@Column(name = "name", nullable = false)
@Column(name = "name")
private String name;
@Column(name = "avatar_url")

View File

@@ -69,8 +69,6 @@ public class AuthService {
User user = User.builder()
.email(email)
.password(passwordEncoder.encode(request.getPassword()))
.name(request.getName())
.emailVerified(false)
.build();
@@ -88,12 +86,12 @@ public class AuthService {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("Invalid email or password"));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("Invalid email or password");
if (!user.isEmailVerified() || user.getPassword() == null) {
throw new IllegalArgumentException("Please verify your email address before logging in");
}
if (!user.isEmailVerified()) {
throw new IllegalArgumentException("Please verify your email address before logging in");
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("Invalid email or password");
}
return generateAuthResponse(user);
@@ -174,6 +172,7 @@ public class AuthService {
User user = userRepository.findById(token.getUserId())
.orElseThrow(() -> new IllegalArgumentException("User not found"));
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
userRepository.save(user);

View File

@@ -28,6 +28,7 @@ public class EmailService {
public void sendVerificationEmail(String to, String name, String token) {
String link = baseUrl + "/verify-email?token=" + token;
String displayName = name != null ? name : to.substring(0, to.indexOf('@'));
String subject = "Verify your Indie account";
String html = """
<!DOCTYPE html>
@@ -61,7 +62,7 @@ public class EmailService {
</table>
</body>
</html>
""".formatted(name, link);
""".formatted(displayName, link);
sendHtml(to, subject, html);
}

View File

@@ -68,7 +68,7 @@ indie:
master-key-path: ${INDIE_ENCRYPTION_KEY_PATH:config/encryption.key}
app:
base-url: ${INDIE_APP_BASE_URL:http://localhost:4200}
base-url: ${INDIE_APP_BASE_URL:https://indie.krrishg.com}
verification-token-expiration-minutes: ${INDIE_VERIFICATION_TOKEN_EXPIRATION:1440}
llm:

View File

@@ -52,8 +52,6 @@ class AuthControllerTest {
RegisterRequest request = new RegisterRequest();
request.setEmail("test@example.com");
request.setPassword("password123");
request.setName("Test");
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
@@ -218,6 +216,7 @@ class AuthControllerTest {
SetPasswordRequest request = new SetPasswordRequest();
request.setSetupToken("setup-token-123");
request.setPassword("new-password");
request.setName("Test");
mockMvc.perform(post("/api/auth/set-password")
.contentType(MediaType.APPLICATION_JSON)
@@ -233,6 +232,7 @@ class AuthControllerTest {
SetPasswordRequest request = new SetPasswordRequest();
request.setSetupToken("bad-token");
request.setPassword("new-password");
request.setName("Test");
mockMvc.perform(post("/api/auth/set-password")
.contentType(MediaType.APPLICATION_JSON)

View File

@@ -43,8 +43,6 @@ class AuthServiceTest {
void registerCreatesUserAndSendsVerification() {
RegisterRequest request = new RegisterRequest();
request.setEmail("new@example.com");
request.setPassword("password123");
request.setName("New User");
RegistrationResponse response = authService.register(request);
@@ -53,12 +51,21 @@ class AuthServiceTest {
assertFalse(userRepository.findByEmail("new@example.com").get().isEmailVerified());
}
@Test
void registerDoesNotSetName() {
RegisterRequest request = new RegisterRequest();
request.setEmail("noname@example.com");
authService.register(request);
User user = userRepository.findByEmail("noname@example.com").get();
assertNull(user.getName());
}
@Test
void registerSendsVerificationEmail() {
RegisterRequest request = new RegisterRequest();
request.setEmail("emailtest@example.com");
request.setPassword("password123");
request.setName("Email Test");
authService.register(request);
@@ -79,8 +86,6 @@ class AuthServiceTest {
RegisterRequest request = new RegisterRequest();
request.setEmail("verified@example.com");
request.setPassword("password123");
request.setName("Should Not Matter");
RegistrationResponse response = authService.register(request);
@@ -100,8 +105,6 @@ class AuthServiceTest {
RegisterRequest request = new RegisterRequest();
request.setEmail("unverified@example.com");
request.setPassword("newpassword");
request.setName("New Name");
RegistrationResponse response = authService.register(request);
@@ -114,8 +117,6 @@ class AuthServiceTest {
void registerNormalizesEmailCase() {
RegisterRequest request = new RegisterRequest();
request.setEmail("UpperCase@Example.com");
request.setPassword("password123");
request.setName("User");
authService.register(request);
@@ -126,11 +127,10 @@ class AuthServiceTest {
void loginWithValidCredentialsAndVerifiedEmailReturnsTokens() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("login@example.com");
reg.setPassword("password123");
reg.setName("Login User");
authService.register(reg);
User user = userRepository.findByEmail("login@example.com").get();
user.setPassword("{stub}password123");
user.setEmailVerified(true);
userRepository.save(user);
@@ -149,8 +149,6 @@ class AuthServiceTest {
void loginThrowsWhenEmailNotVerified() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("unverified@example.com");
reg.setPassword("password123");
reg.setName("Unverified");
authService.register(reg);
LoginRequest login = new LoginRequest();
@@ -166,11 +164,10 @@ class AuthServiceTest {
void loginThrowsOnWrongPassword() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("wrongpw@example.com");
reg.setPassword("correctpw");
reg.setName("User");
authService.register(reg);
User user = userRepository.findByEmail("wrongpw@example.com").get();
user.setPassword("{stub}correctpw");
user.setEmailVerified(true);
userRepository.save(user);
@@ -315,11 +312,13 @@ class AuthServiceTest {
SetPasswordRequest request = new SetPasswordRequest();
request.setSetupToken("setup-token-123");
request.setPassword("new-password");
request.setName("New Name");
authService.setPassword(request);
User updatedUser = userRepository.findById(user.getId()).get();
assertTrue(updatedUser.getPassword().contains("new-password"));
assertEquals("New Name", updatedUser.getName());
VerificationToken updatedToken = tokenRepository.findByToken("verify-token").get();
assertTrue(updatedToken.isSetupTokenUsed());
@@ -480,8 +479,6 @@ class AuthServiceTest {
void refreshTokenReturnsNewTokens() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("refresh@example.com");
reg.setPassword("password123");
reg.setName("Refresh User");
authService.register(reg);
User user = userRepository.findByEmail("refresh@example.com").get();

View File

@@ -18,6 +18,11 @@ export const routes: Routes = [
loadComponent: () => import('./auth/register/register.component').then(m => m.RegisterComponent),
canActivate: [LoginGuard],
},
{
path: 'resend-verification',
loadComponent: () => import('./auth/resend-verification/resend-verification.component').then(m => m.ResendVerificationComponent),
canActivate: [LoginGuard],
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),

View File

@@ -64,6 +64,9 @@ import { NgIf } from '@angular/common';
Don't have an account?
<a routerLink="/register" class="text-indigo-600 font-medium hover:underline">Register</a>
</p>
<p class="text-center mt-2 text-sm text-gray-500">
<a routerLink="/resend-verification" class="text-indigo-600 font-medium hover:underline">Resend verification email</a>
</p>
</mat-card>
</div>
`,

View File

@@ -26,12 +26,6 @@ import { AuthService } from '../../core/services/auth.service';
</div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Name</mat-label>
<input matInput formControlName="name" placeholder="Your name" />
<mat-error *ngIf="registerForm.get('name')?.hasError('required')">Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="you@example.com" />
@@ -39,13 +33,6 @@ import { AuthService } from '../../core/services/auth.service';
<mat-error *ngIf="registerForm.get('email')?.hasError('email')">Invalid email format</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="registerForm.get('password')?.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="registerForm.get('password')?.hasError('minlength')">At least 8 characters</mat-error>
</mat-form-field>
<div *ngIf="error" class="text-red-600 text-sm">{{ error }}</div>
<button mat-flat-button color="primary" type="submit" [disabled]="loading" class="w-full py-2">
@@ -66,9 +53,7 @@ import { AuthService } from '../../core/services/auth.service';
})
export class RegisterComponent {
registerForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
loading = false;
error = '';
@@ -83,8 +68,8 @@ export class RegisterComponent {
if (this.registerForm.invalid) return;
this.loading = true;
this.error = '';
const { name, email, password } = this.registerForm.value;
this.authService.register(name!, email!, password!).subscribe({
const { email } = this.registerForm.value;
this.authService.register(email!).subscribe({
next: () => this.router.navigate(['/verify-email'], { queryParams: { email: email! } }),
error: (err) => {
this.error = err.error?.message || err.error?.error || 'Registration failed';

View File

@@ -0,0 +1,83 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
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 { NgIf } from '@angular/common';
import { AuthService } from '../../core/services/auth.service';
@Component({
selector: 'app-resend-verification',
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>
<p class="text-gray-500 mt-1">Resend verification email</p>
</div>
<form [formGroup]="resendForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="you@example.com" />
<mat-error *ngIf="resendForm.get('email')?.hasError('required')">Email is required</mat-error>
<mat-error *ngIf="resendForm.get('email')?.hasError('email')">Invalid email format</mat-error>
</mat-form-field>
<div *ngIf="message" [class.text-green-600]="success" [class.text-red-600]="!success" class="text-sm text-center">{{ message }}</div>
<button mat-flat-button color="primary" type="submit" [disabled]="loading" class="w-full py-2">
<span class="flex items-center justify-center">
<mat-spinner *ngIf="loading" diameter="20" class="mr-2"></mat-spinner>
{{ loading ? 'Sending...' : 'Send verification email' }}
</span>
</button>
</form>
<p class="text-center mt-6 text-sm text-gray-500">
<a routerLink="/login" class="text-indigo-600 font-medium hover:underline">Back to login</a>
</p>
</mat-card>
</div>
`,
})
export class ResendVerificationComponent {
resendForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
loading = false;
message = '';
success = false;
constructor(
private fb: FormBuilder,
private authService: AuthService,
) {}
onSubmit(): void {
if (this.resendForm.invalid) return;
this.loading = true;
this.message = '';
this.authService.resendVerification(this.resendForm.value.email!).subscribe({
next: () => {
this.message = 'If this email is registered, a verification link has been sent.';
this.success = true;
this.loading = false;
},
error: () => {
this.message = 'Failed to send. Please try again later.';
this.success = false;
this.loading = false;
},
});
}
}

View File

@@ -62,8 +62,14 @@ type PageState = 'check-email' | 'verifying' | 'verified' | 'set-password' | 'er
</div>
<div *ngIf="state === 'set-password'">
<h2 class="text-xl font-semibold text-gray-800 mb-4 text-center">Set your password</h2>
<h2 class="text-xl font-semibold text-gray-800 mb-4 text-center">Set your name and password</h2>
<form [formGroup]="passwordForm" (ngSubmit)="onSetPassword()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Name</mat-label>
<input matInput formControlName="name" placeholder="Your name" />
<mat-error *ngIf="passwordForm.get('name')?.hasError('required')">Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>New Password</mat-label>
<input matInput type="password" formControlName="password" />
@@ -120,6 +126,7 @@ export class VerifyEmailComponent implements OnInit {
passwordError = '';
passwordForm = this.fb.group({
name: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
}, { validators: this.passwordsMatch });
@@ -211,7 +218,7 @@ export class VerifyEmailComponent implements OnInit {
this.passwordLoading = true;
this.passwordError = '';
this.authService.setPassword(this.setupToken, this.passwordForm.value.password!).subscribe({
this.authService.setPassword(this.setupToken, this.passwordForm.value.password!, this.passwordForm.value.name!).subscribe({
next: () => {
this.router.navigate(['/login'], { queryParams: { verified: 'true' } });
},

View File

@@ -14,8 +14,8 @@ export class AuthService {
constructor(private http: HttpClient) {}
register(name: string, email: string, password: string): Observable<{ message: string }> {
return this.http.post<{ message: string }>('/api/auth/register', { name, email, password });
register(email: string): Observable<{ message: string }> {
return this.http.post<{ message: string }>('/api/auth/register', { email });
}
login(email: string, password: string): Observable<AuthResponse> {
@@ -41,8 +41,8 @@ export class AuthService {
return this.http.post<{ message: string }>('/api/auth/resend-verification', { email });
}
setPassword(setupToken: string, password: string): Observable<void> {
return this.http.post<void>('/api/auth/set-password', { setupToken, password });
setPassword(setupToken: string, password: string, name: string): Observable<void> {
return this.http.post<void>('/api/auth/set-password', { setupToken, password, name });
}
getAccessToken(): string | null {