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.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
@Data @Data
@@ -11,11 +10,4 @@ public class RegisterRequest {
@NotBlank(message = "Email is required") @NotBlank(message = "Email is required")
@Email(message = "Invalid email format") @Email(message = "Invalid email format")
private String email; 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 @NotBlank
@Size(min = 8, max = 100) @Size(min = 8, max = 100)
private String password; private String password;
@NotBlank(message = "Name is required")
private String name;
} }

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ indie:
master-key-path: ${INDIE_ENCRYPTION_KEY_PATH:config/encryption.key} master-key-path: ${INDIE_ENCRYPTION_KEY_PATH:config/encryption.key}
app: 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} verification-token-expiration-minutes: ${INDIE_VERIFICATION_TOKEN_EXPIRATION:1440}
llm: llm:

View File

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

View File

@@ -43,8 +43,6 @@ class AuthServiceTest {
void registerCreatesUserAndSendsVerification() { void registerCreatesUserAndSendsVerification() {
RegisterRequest request = new RegisterRequest(); RegisterRequest request = new RegisterRequest();
request.setEmail("new@example.com"); request.setEmail("new@example.com");
request.setPassword("password123");
request.setName("New User");
RegistrationResponse response = authService.register(request); RegistrationResponse response = authService.register(request);
@@ -53,12 +51,21 @@ class AuthServiceTest {
assertFalse(userRepository.findByEmail("new@example.com").get().isEmailVerified()); 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 @Test
void registerSendsVerificationEmail() { void registerSendsVerificationEmail() {
RegisterRequest request = new RegisterRequest(); RegisterRequest request = new RegisterRequest();
request.setEmail("emailtest@example.com"); request.setEmail("emailtest@example.com");
request.setPassword("password123");
request.setName("Email Test");
authService.register(request); authService.register(request);
@@ -79,8 +86,6 @@ class AuthServiceTest {
RegisterRequest request = new RegisterRequest(); RegisterRequest request = new RegisterRequest();
request.setEmail("verified@example.com"); request.setEmail("verified@example.com");
request.setPassword("password123");
request.setName("Should Not Matter");
RegistrationResponse response = authService.register(request); RegistrationResponse response = authService.register(request);
@@ -100,8 +105,6 @@ class AuthServiceTest {
RegisterRequest request = new RegisterRequest(); RegisterRequest request = new RegisterRequest();
request.setEmail("unverified@example.com"); request.setEmail("unverified@example.com");
request.setPassword("newpassword");
request.setName("New Name");
RegistrationResponse response = authService.register(request); RegistrationResponse response = authService.register(request);
@@ -114,8 +117,6 @@ class AuthServiceTest {
void registerNormalizesEmailCase() { void registerNormalizesEmailCase() {
RegisterRequest request = new RegisterRequest(); RegisterRequest request = new RegisterRequest();
request.setEmail("UpperCase@Example.com"); request.setEmail("UpperCase@Example.com");
request.setPassword("password123");
request.setName("User");
authService.register(request); authService.register(request);
@@ -126,11 +127,10 @@ class AuthServiceTest {
void loginWithValidCredentialsAndVerifiedEmailReturnsTokens() { void loginWithValidCredentialsAndVerifiedEmailReturnsTokens() {
RegisterRequest reg = new RegisterRequest(); RegisterRequest reg = new RegisterRequest();
reg.setEmail("login@example.com"); reg.setEmail("login@example.com");
reg.setPassword("password123");
reg.setName("Login User");
authService.register(reg); authService.register(reg);
User user = userRepository.findByEmail("login@example.com").get(); User user = userRepository.findByEmail("login@example.com").get();
user.setPassword("{stub}password123");
user.setEmailVerified(true); user.setEmailVerified(true);
userRepository.save(user); userRepository.save(user);
@@ -149,8 +149,6 @@ class AuthServiceTest {
void loginThrowsWhenEmailNotVerified() { void loginThrowsWhenEmailNotVerified() {
RegisterRequest reg = new RegisterRequest(); RegisterRequest reg = new RegisterRequest();
reg.setEmail("unverified@example.com"); reg.setEmail("unverified@example.com");
reg.setPassword("password123");
reg.setName("Unverified");
authService.register(reg); authService.register(reg);
LoginRequest login = new LoginRequest(); LoginRequest login = new LoginRequest();
@@ -166,11 +164,10 @@ class AuthServiceTest {
void loginThrowsOnWrongPassword() { void loginThrowsOnWrongPassword() {
RegisterRequest reg = new RegisterRequest(); RegisterRequest reg = new RegisterRequest();
reg.setEmail("wrongpw@example.com"); reg.setEmail("wrongpw@example.com");
reg.setPassword("correctpw");
reg.setName("User");
authService.register(reg); authService.register(reg);
User user = userRepository.findByEmail("wrongpw@example.com").get(); User user = userRepository.findByEmail("wrongpw@example.com").get();
user.setPassword("{stub}correctpw");
user.setEmailVerified(true); user.setEmailVerified(true);
userRepository.save(user); userRepository.save(user);
@@ -315,11 +312,13 @@ class AuthServiceTest {
SetPasswordRequest request = new SetPasswordRequest(); SetPasswordRequest request = new SetPasswordRequest();
request.setSetupToken("setup-token-123"); request.setSetupToken("setup-token-123");
request.setPassword("new-password"); request.setPassword("new-password");
request.setName("New Name");
authService.setPassword(request); authService.setPassword(request);
User updatedUser = userRepository.findById(user.getId()).get(); User updatedUser = userRepository.findById(user.getId()).get();
assertTrue(updatedUser.getPassword().contains("new-password")); assertTrue(updatedUser.getPassword().contains("new-password"));
assertEquals("New Name", updatedUser.getName());
VerificationToken updatedToken = tokenRepository.findByToken("verify-token").get(); VerificationToken updatedToken = tokenRepository.findByToken("verify-token").get();
assertTrue(updatedToken.isSetupTokenUsed()); assertTrue(updatedToken.isSetupTokenUsed());
@@ -480,8 +479,6 @@ class AuthServiceTest {
void refreshTokenReturnsNewTokens() { void refreshTokenReturnsNewTokens() {
RegisterRequest reg = new RegisterRequest(); RegisterRequest reg = new RegisterRequest();
reg.setEmail("refresh@example.com"); reg.setEmail("refresh@example.com");
reg.setPassword("password123");
reg.setName("Refresh User");
authService.register(reg); authService.register(reg);
User user = userRepository.findByEmail("refresh@example.com").get(); 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), loadComponent: () => import('./auth/register/register.component').then(m => m.RegisterComponent),
canActivate: [LoginGuard], canActivate: [LoginGuard],
}, },
{
path: 'resend-verification',
loadComponent: () => import('./auth/resend-verification/resend-verification.component').then(m => m.ResendVerificationComponent),
canActivate: [LoginGuard],
},
{ {
path: 'dashboard', path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent), 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? Don't have an account?
<a routerLink="/register" class="text-indigo-600 font-medium hover:underline">Register</a> <a routerLink="/register" class="text-indigo-600 font-medium hover:underline">Register</a>
</p> </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> </mat-card>
</div> </div>
`, `,

View File

@@ -26,12 +26,6 @@ import { AuthService } from '../../core/services/auth.service';
</div> </div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-4"> <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-form-field appearance="outline" class="w-full">
<mat-label>Email</mat-label> <mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="you@example.com" /> <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-error *ngIf="registerForm.get('email')?.hasError('email')">Invalid email format</mat-error>
</mat-form-field> </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> <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"> <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 { export class RegisterComponent {
registerForm = this.fb.group({ registerForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
}); });
loading = false; loading = false;
error = ''; error = '';
@@ -83,8 +68,8 @@ export class RegisterComponent {
if (this.registerForm.invalid) return; if (this.registerForm.invalid) return;
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
const { name, email, password } = this.registerForm.value; const { email } = this.registerForm.value;
this.authService.register(name!, email!, password!).subscribe({ this.authService.register(email!).subscribe({
next: () => this.router.navigate(['/verify-email'], { queryParams: { email: email! } }), next: () => this.router.navigate(['/verify-email'], { queryParams: { email: email! } }),
error: (err) => { error: (err) => {
this.error = err.error?.message || err.error?.error || 'Registration failed'; 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>
<div *ngIf="state === 'set-password'"> <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"> <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-form-field appearance="outline" class="w-full">
<mat-label>New Password</mat-label> <mat-label>New Password</mat-label>
<input matInput type="password" formControlName="password" /> <input matInput type="password" formControlName="password" />
@@ -120,6 +126,7 @@ export class VerifyEmailComponent implements OnInit {
passwordError = ''; passwordError = '';
passwordForm = this.fb.group({ passwordForm = this.fb.group({
name: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(8)]], password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required], confirmPassword: ['', Validators.required],
}, { validators: this.passwordsMatch }); }, { validators: this.passwordsMatch });
@@ -211,7 +218,7 @@ export class VerifyEmailComponent implements OnInit {
this.passwordLoading = true; this.passwordLoading = true;
this.passwordError = ''; 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: () => { next: () => {
this.router.navigate(['/login'], { queryParams: { verified: 'true' } }); this.router.navigate(['/login'], { queryParams: { verified: 'true' } });
}, },

View File

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