Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ src/modules/<domain>/
- **Containerization:** Docker, Docker Compose
- **Architecture:** Domain-Driven Design (DDD)

## 🚨 Error Handling & Exceptions

VolunChain uses a standardized error handling system with domain-specific exceptions:

### Allowed Exceptions

- **ValidationException (400)**: Invalid input data or DTO validation failures
- **AuthenticationException (401)**: Invalid credentials, missing tokens, or authentication failures
- **AuthorizationException (403)**: Insufficient permissions or access denied
- **ConflictException (409)**: Resource conflicts (e.g., duplicate emails, unique constraint violations)
- **InternalServerException (500)**: Unexpected system errors or database failures

Comment on lines +92 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Include NotFoundException (404) in the allowed list.

Docs omit 404 while the code exports NotFoundException. Add it for consistency.

 - **ConflictException (409)**: Resource conflicts (e.g., duplicate emails, unique constraint violations)
 - **InternalServerException (500)**: Unexpected system errors or database failures
+- **NotFoundException (404)**: Requested resource does not exist

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
πŸͺ› LanguageTool

[grammar] ~92-~92: There might be a mistake here.
Context: ...id input data or DTO validation failures - AuthenticationException (401): Invalid...

(QB_NEW_EN)


[grammar] ~93-~93: There might be a mistake here.
Context: ...ssing tokens, or authentication failures - AuthorizationException (403): Insuffic...

(QB_NEW_EN)


[grammar] ~94-~94: There might be a mistake here.
Context: ...nsufficient permissions or access denied - ConflictException (409): Resource conf...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ...te emails, unique constraint violations) - InternalServerException (500): Unexpec...

(QB_NEW_EN)

πŸ€– Prompt for AI Agents
In README.md around lines 92 to 97, the error list is missing the
NotFoundException (404) despite the code exporting it; add a new bullet to the
list such as "- **NotFoundException (404)**: Resource not found or does not
exist" (matching style of other entries) so the docs reflect the actual exported
exceptions and HTTP semantics.

### Error Response Format

All errors follow this consistent JSON structure:

```json
{
"statusCode": 400,
"errorCode": "VALIDATION_ERROR",
"message": "DTO validation failed",
"details": { "errors": [...] },
"traceId": "abc-123-def"
}
```

### Implementation Rules

1. **Controllers**: Never throw raw `Error` objects or craft custom JSON responses
2. **Use Cases**: Surface domain exceptions only, no ad-hoc error handling
3. **Repositories**: Use `prismaGuard()` wrapper to catch and map Prisma errors
4. **Global Handler**: All exceptions are processed by the unified error handler
5. **Validation**: DTO failures automatically become `ValidationException`

### Example Usage

```typescript
// βœ… Correct - Throw domain exceptions
if (!user) {
throw new ValidationException("User not found");
}

// ❌ Incorrect - Raw errors or custom responses
if (!user) {
throw new Error("User not found");
// or
res.status(400).json({ error: "User not found" });
}
```
Comment on lines +124 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix exception type in example.

β€œUser not found” β†’ NotFoundException (404), not ValidationException.

-// βœ… Correct - Throw domain exceptions
-if (!user) {
-  throw new ValidationException("User not found");
-}
+// βœ… Correct - Throw domain exceptions
+if (!user) {
+  throw new NotFoundException("User not found");
+}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!user) {
throw new ValidationException("User not found");
}
// ❌ Incorrect - Raw errors or custom responses
if (!user) {
throw new Error("User not found");
// or
res.status(400).json({ error: "User not found" });
}
```
// βœ… Correct - Throw domain exceptions
if (!user) {
throw new NotFoundException("User not found");
}
// ❌ Incorrect - Raw errors or custom responses
if (!user) {
throw new Error("User not found");
// or
res.status(400).json({ error: "User not found" });
}
πŸ€– Prompt for AI Agents
In README.md around lines 124 to 134, the example throws the wrong exception
type for a missing user: it currently shows ValidationException (and contrasts
with raw Error/JSON). Replace the ValidationException with a NotFoundException
(HTTP 404) in the example, and update any surrounding explanatory text to
reflect that a missing resource should map to NotFound/404 rather than a
validation error; keep the rest of the example behavior the same.


---

## πŸš€ Quick Start
Expand Down Expand Up @@ -236,6 +286,19 @@ npm run db:seed

---

## πŸ”Œ Supabase Integration

This project uses Supabase for external data access and future integrations.

Update your `.env` file with:

```bash
SUPABASE_URL=...
SUPABASE_ANON_KEY=...
```
Comment on lines +289 to +298
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Verify variables are used in code and present in sample envs

Ensure the code expects these exact names and that .env.example/.env.test include them.


🏁 Script executed:

#!/bin/bash
# Find references to Supabase env vars across the repo
rg -nP '\bSUPABASE_(URL|ANON_KEY|SERVICE_ROLE_KEY)\b' -C2

# Confirm they exist in sample env files
fd -H '^\.env(\.example|\.test)?$' -a | xargs -I{} sh -c 'echo "---- {}"; cat "{}" | rg -n "SUPABASE_" || true'

# Check docker compose files for propagation
fd -a 'docker-compose*.yml' | xargs -I{} sh -c 'echo "---- {}"; rg -n "SUPABASE_" "{}" || true'

Length of output: 1122


Add missing Supabase env vars to sample env files

  • .env.test is currently missing SUPABASE_URL and SUPABASE_ANON_KEY which are required by src/config/supabase.ts; add them with appropriate test placeholders.
  • Confirm docker-compose.yml propagates these vars to any Supabase‐dependent services (via env_file or environment).
🧰 Tools
πŸͺ› LanguageTool

[grammar] ~239-~239: There might be a mistake here.
Context: ...eed ``` --- ## πŸ”Œ Supabase Integration This project uses Supabase for external ...

(QB_NEW_EN)


Comment on lines +289 to +299
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

⚠️ Potential issue

Avoid README/readme duplication (case conflict).

Both README.md and readme.md exist; this breaks clones on case-insensitive FS (macOS/Windows) and confuses links. Keep a single README.md.

🧰 Tools
πŸͺ› LanguageTool

[grammar] ~289-~289: There might be a mistake here.
Context: ...eed ``` --- ## πŸ”Œ Supabase Integration This project uses Supabase for external ...

(QB_NEW_EN)

πŸ€– Prompt for AI Agents
In README.md around lines 289–299 there is a duplicate lower-case readme
(readme.md) in the repo which causes conflicts on case-insensitive filesystems;
consolidate content into a single README.md, remove or rename the redundant
readme.md from the repository, update any internal links or references to point
to README.md, and commit the change with a proper repo-case fix sequence so the
filename case change is recorded on case-insensitive systems.

---

## πŸ“ Module Overview

### Core Modules
Expand Down
50 changes: 50 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ src/modules/<domain>/
- **Containerization:** Docker, Docker Compose
- **Architecture:** Domain-Driven Design (DDD)

## 🚨 Error Handling & Exceptions

VolunChain uses a standardized error handling system with domain-specific exceptions:

### Allowed Exceptions

- **ValidationException (400)**: Invalid input data or DTO validation failures
- **AuthenticationException (401)**: Invalid credentials, missing tokens, or authentication failures
- **AuthorizationException (403)**: Insufficient permissions or access denied
- **ConflictException (409)**: Resource conflicts (e.g., duplicate emails, unique constraint violations)
- **InternalServerException (500)**: Unexpected system errors or database failures

### Error Response Format

All errors follow this consistent JSON structure:

```json
{
"statusCode": 400,
"errorCode": "VALIDATION_ERROR",
"message": "DTO validation failed",
"details": { "errors": [...] },
"traceId": "abc-123-def"
}
```

### Implementation Rules

1. **Controllers**: Never throw raw `Error` objects or craft custom JSON responses
2. **Use Cases**: Surface domain exceptions only, no ad-hoc error handling
3. **Repositories**: Use `prismaGuard()` wrapper to catch and map Prisma errors
4. **Global Handler**: All exceptions are processed by the unified error handler
5. **Validation**: DTO failures automatically become `ValidationException`

### Example Usage

```typescript
// βœ… Correct - Throw domain exceptions
if (!user) {
throw new ValidationException("User not found");
}

// ❌ Incorrect - Raw errors or custom responses
if (!user) {
throw new Error("User not found");
// or
res.status(400).json({ error: "User not found" });
}
```
Comment on lines +120 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Example should not use ValidationException for missing user.

β€œUser not found” maps to NotFound (404), not Validation (400).

-// βœ… Correct - Throw domain exceptions
-if (!user) {
-  throw new ValidationException("User not found");
-}
+// βœ… Correct - Throw domain exceptions
+if (!user) {
+  throw new NotFoundException("User not found");
+}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Example Usage
```typescript
// βœ… Correct - Throw domain exceptions
if (!user) {
throw new ValidationException("User not found");
}
// ❌ Incorrect - Raw errors or custom responses
if (!user) {
throw new Error("User not found");
// or
res.status(400).json({ error: "User not found" });
}
```
// βœ… Correct - Throw domain exceptions
if (!user) {
throw new NotFoundException("User not found");
}
πŸ€– Prompt for AI Agents
In readme.md around lines 120 to 134, the example incorrectly uses
ValidationException for a missing user; change the example to throw the correct
domain NotFound exception (or a NotFoundException) to map to 404, and update the
inline comments to show that missing resources should throw NotFound rather than
Validation; keep the β€œβŒ Incorrect” section showing raw Error or manual
res.status usage as bad practice but replace the ValidationExample with a
NotFound example.


---

## πŸš€ Quick Start
Expand Down
121 changes: 55 additions & 66 deletions src/modules/auth/presentation/controllers/Auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Request, Response } from "express";
import { Request, Response, NextFunction } from "express";

// imports for DTO validator
import { plainToInstance } from "class-transformer";
Expand All @@ -21,6 +21,9 @@ import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase";
import { ValidateWalletFormatUseCase } from "../../use-cases/wallet-format-validation.usecase";
import { VerifyWalletUseCase } from "../../use-cases/verify-wallet.usecase";

// Domain exceptions
import { ValidationException, AuthenticationException, InternalServerException } from "../../../../shared/exceptions";

const userRepository = new PrismaUserRepository();
const sendVerificationEmailUseCase = new SendVerificationEmailUseCase(
userRepository
Expand All @@ -35,130 +38,116 @@ const verifyWalletUseCase = new VerifyWalletUseCase();
// DTO validator
async function validateOr400<T>(
Cls: new () => T,
payload: unknown,
res: Response
): Promise<T | undefined> {
payload: unknown
): Promise<T> {
const dto = plainToInstance(Cls, payload);
const errors = await validate(dto as object, {
whitelist: true,
forbidNonWhitelisted: true,
});

// dto not verified, throw a Bad Request
// DTO not verified, throw ValidationException
if (errors.length) {
res.status(400).json({ message: "Validation failed", errors });
return;
const formattedErrors = errors.map(error => ({
property: error.property,
value: error.value,
constraints: error.constraints ? Object.values(error.constraints) : [],
}));
throw new ValidationException('DTO validation failed', { errors: formattedErrors });
Comment on lines +51 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not include raw input values in validation error details (PII leak).

value: error.value can expose secrets (e.g., passwords). Remove it from the response details.

-    const formattedErrors = errors.map(error => ({
-      property: error.property,
-      value: error.value,
-      constraints: error.constraints ? Object.values(error.constraints) : [],
-    }));
+    const formattedErrors = errors.map(error => ({
+      property: error.property,
+      constraints: error.constraints ? Object.values(error.constraints) : [],
+    }));
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formattedErrors = errors.map(error => ({
property: error.property,
value: error.value,
constraints: error.constraints ? Object.values(error.constraints) : [],
}));
throw new ValidationException('DTO validation failed', { errors: formattedErrors });
const formattedErrors = errors.map(error => ({
property: error.property,
constraints: error.constraints ? Object.values(error.constraints) : [],
}));
throw new ValidationException('DTO validation failed', { errors: formattedErrors });
πŸ€– Prompt for AI Agents
In src/modules/auth/presentation/controllers/Auth.controller.ts around lines 51
to 56, the validation error payload currently includes raw input values (value:
error.value) which can leak PII/secrets; remove the value field from
formattedErrors and only include non-sensitive fields (e.g., property and
constraints as an array). Update the formattedErrors mapping to omit
error.value, adjust any ValidationException payload typing if necessary, and
ensure no other code paths log or return raw DTO values in validation errors.

}

return dto;
}

const register = async (req: Request, res: Response) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Ensure errors reach the global handler: pass next(err) from async handlers.

Throwing inside async handlers is only auto-forwarded in Express 5 (or with express-async-errors). To be safe and explicit, accept NextFunction and call next(err).

If you’re already on Express 5 or using express-async-errors, you can drop the try/catch entirely. Otherwise, apply:

-const register = async (req: Request, res: Response) => {
+const register = async (req: Request, res: Response, next: NextFunction) => {
   try {
     const dto = await validateOr400(RegisterDto, req.body);
     await sendVerificationEmailUseCase.execute({ email: dto.email });
     res.status(200).json({ message: "Verification email sent" });
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

-const login = async (req: Request, res: Response) => {
+const login = async (req: Request, res: Response, next: NextFunction) => {
   try {
     const dto = await validateOr400(LoginDto, req.body);
     // TODO: Implement Wallet auth logic as a use case
     throw new InternalServerException('Login service temporarily disabled', {
       error: "Wallet auth logic not implemented yet"
     });
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

-const resendVerificationEmail = async (req: Request, res: Response) => {
+const resendVerificationEmail = async (req: Request, res: Response, next: NextFunction) => {
   try {
     const dto = await validateOr400(ResendVerificationDTO, req.body);
     await resendVerificationEmailUseCase.execute({ email: dto.email });
     res.status(200).json({ message: "Verification email resent" });
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

-const verifyEmail = async (req: Request, res: Response) => {
+const verifyEmail = async (req: Request, res: Response, next: NextFunction) => {
   try {
     // ...
     const result = await verifyEmailUseCase.execute({ token });
     const status = result.success ? 200 : 400;
     res.status(status).json(result);
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

-const verifyWallet = async (req: Request, res: Response) => {
+const verifyWallet = async (req: Request, res: Response, next: NextFunction) => {
   try {
     const dto = await validateOr400(VerifyWalletDto, req.body);
     const result = await verifyWalletUseCase.execute(dto);
     const status = result.verified ? 200 : 400;
     res.status(status).json(result);
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

-const validateWalletFormat = async (req: Request, res: Response) => {
+const validateWalletFormat = async (req: Request, res: Response, next: NextFunction) => {
   try {
     const dto = await validateOr400(ValidateWalletFormatDto, req.body);
     const result = await validateWalletFormatUseCase.execute(dto);
     const status = result.valid ? 200 : 400;
     res.status(status).json(result);
   } catch (err) {
-    // Let the global error handler deal with it
-    throw err;
+    next(err);
   }
 };

Verify Express version and presence of express-async-errors:

Also applies to: 69-72, 75-75, 83-86, 89-89, 96-99, 102-102, 121-124, 127-127, 134-137, 140-140, 148-151


🏁 Script executed:

#!/bin/bash
# Show Express version and whether express-async-errors is used
jq -r '.dependencies.express, .devDependencies.express' package.json
jq -r '.dependencies["express-async-errors"], .devDependencies["express-async-errors"]' package.json

Length of output: 196


Add explicit error forwarding in async controllers
Import NextFunction from 'express' and update each async handler in src/modules/auth/presentation/controllers/Auth.controller.ts (register, login, resendVerificationEmail, verifyEmail, verifyWallet, validateWalletFormat) to include the next: NextFunction parameter and replace throw err in their catch blocks with next(err). This ensures errors bubble up to the global handler under Express 4.x.

πŸ€– Prompt for AI Agents
In src/modules/auth/presentation/controllers/Auth.controller.ts around line 62,
the async controller signatures lack the NextFunction parameter and currently
use `throw err` in catch blocks; import `NextFunction` from 'express', update
each async handler (register, login, resendVerificationEmail, verifyEmail,
verifyWallet, validateWalletFormat) to accept `next: NextFunction` as the third
parameter, and replace all `throw err` occurrences inside their catch blocks
with `next(err)` so errors are forwarded to Express's global error handler.

const dto = await validateOr400(RegisterDto, req.body, res);
if (!dto) return;

try {
const dto = await validateOr400(RegisterDto, req.body);

// Send verification email to provided address
await sendVerificationEmailUseCase.execute({ email: dto.email });
res.status(200).json({ message: "Verification email sent" });
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to send verification email";
const status = message === "User not found" ? 400 : 500;
res.status(status).json({ error: message });
// Let the global error handler deal with it
throw err;
}
};

const login = async (req: Request, res: Response) => {
const dto = await validateOr400(LoginDto, req.body, res);
if (!dto) return;
try {
const dto = await validateOr400(LoginDto, req.body);

// TODO: Implement Wallet auth logic as a use case
res.status(501).json({
message: "Login service temporarily disabled",
error: "Wallet auth logic not implemented yet",
});
// TODO: Implement Wallet auth logic as a use case
throw new InternalServerException('Login service temporarily disabled', {
error: "Wallet auth logic not implemented yet"
});
} catch (err) {
// Let the global error handler deal with it
throw err;
}
};

const resendVerificationEmail = async (req: Request, res: Response) => {
const dto = await validateOr400(ResendVerificationDTO, req.body, res);
if (!dto) return;

try {
const dto = await validateOr400(ResendVerificationDTO, req.body);

// Resends verification email to provided address
await resendVerificationEmailUseCase.execute({ email: dto.email });
res.status(200).json({ message: "Verification email resent" });
} catch (err) {
const message =
err instanceof Error
? err.message
: "Failed to resend verification email";
const status = message === "User not found" ? 404 : 500;
res.status(status).json({ error: message });
// Let the global error handler deal with it
throw err;
}
};

const verifyEmail = async (req: Request, res: Response) => {
const tokenParam =
typeof req.params.token === "string" ? req.params.token : undefined;
const tokenQuery =
typeof req.query.token === "string"
? (req.query.token as string)
: undefined;
const token = tokenParam || tokenQuery;

// if token is not given in the request
if (!token) {
res.status(400).json({
success: false,
message: "Token in URL is required",
verified: false,
});
return;
}

try {
const tokenParam =
typeof req.params.token === "string" ? req.params.token : undefined;
const tokenQuery =
typeof req.query.token === "string"
? (req.query.token as string)
: undefined;
const token = tokenParam || tokenQuery;

// if token is not given in the request
if (!token) {
throw new ValidationException('Token in URL is required');
}

// Verifies email using use case
const result = await verifyEmailUseCase.execute({ token });
const status = result.success ? 200 : 400;
res.status(status).json(result);
} catch {
res.status(400).json({
success: false,
message: "Invalid or expired verification token",
verified: false,
});
} catch (err) {
Comment on lines 117 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Use domain exceptions for negative paths to keep responses unified.

Instead of returning 400 JSON with ad-hoc shapes, throw domain exceptions and let the global error handler format the response.

-    const result = await verifyEmailUseCase.execute({ token });
-    const status = result.success ? 200 : 400;
-    res.status(status).json(result);
+    const result = await verifyEmailUseCase.execute({ token });
+    if (!result.success) {
+      throw new ValidationException("Email verification failed");
+    }
+    res.status(200).json(result);
-    const result = await verifyWalletUseCase.execute(dto);
-    const status = result.verified ? 200 : 400;
-    res.status(status).json(result);
+    const result = await verifyWalletUseCase.execute(dto);
+    if (!result.verified) {
+      throw new ValidationException("Wallet verification failed");
+    }
+    res.status(200).json(result);
-    const result = await validateWalletFormatUseCase.execute(dto);
-    const status = result.valid ? 200 : 400;
-    res.status(status).json(result);
+    const result = await validateWalletFormatUseCase.execute(dto);
+    if (!result.valid) {
+      throw new ValidationException("Invalid wallet format");
+    }
+    res.status(200).json(result);

Also applies to: 131-133, 145-147

// Let the global error handler deal with it
throw err;
}
};

const verifyWallet = async (req: Request, res: Response) => {
const dto = await validateOr400(VerifyWalletDto, req.body, res);
if (!dto) return;

try {
const dto = await validateOr400(VerifyWalletDto, req.body);

const result = await verifyWalletUseCase.execute(dto);
const status = result.verified ? 200 : 400;
res.status(status).json(result);
} catch (err) {
const message =
err instanceof Error ? err.message : "Wallet verification failed";
res.status(500).json({ error: message });
// Let the global error handler deal with it
throw err;
}
};

const validateWalletFormat = async (req: Request, res: Response) => {
const dto = await validateOr400(ValidateWalletFormatDto, req.body, res);
if (!dto) return;

try {
const dto = await validateOr400(ValidateWalletFormatDto, req.body);

// Validates wallet format using use case
const result = await validateWalletFormatUseCase.execute(dto);
const status = result.valid ? 200 : 400;
res.status(status).json(result);
} catch (err) {
const message =
err instanceof Error ? err.message : "Wallet format validation failed";
res.status(500).json({ error: message });
// Let the global error handler deal with it
throw err;
}
};

Expand Down
42 changes: 11 additions & 31 deletions src/modules/auth/use-cases/email-verification.usecase.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,19 @@
import { IUserRepository } from "../../../repository/IUserRepository";
import { randomBytes } from "crypto";
import { sendVerificationEmail } from "../../../utils/email.utils";
import { IUserRepository } from "../../user/domain/interfaces/IUserRepository";
import { ValidationException, ConflictException } from "../../../../shared/exceptions";

export class EmailVerificationUseCase {
constructor(private userRepository: IUserRepository) {}

async sendVerificationEmail(email: string): Promise<void> {
async execute({ email }: { email: string }): Promise<void> {
const user = await this.userRepository.findByEmail(email);
if (!user) throw new Error("User not found");
if (user.isVerified) throw new Error("User is already verified");

const token = randomBytes(32).toString("hex");
const expires = new Date();
expires.setHours(expires.getHours() + 1);

await this.userRepository.updateVerificationToken(user.id, token, expires);

const verificationLink = `http://localhost:3000/auth/verify-email?token=${token}`;

await sendVerificationEmail(user.email, verificationLink);
}

async verifyEmail(token: string): Promise<void> {
const user = await this.userRepository.findByVerificationToken(token);
if (!user) throw new Error("Invalid or expired token");

if (
user.verificationTokenExpires &&
new Date() > user.verificationTokenExpires
) {
throw new Error(
"Token expired. Please request a new verification email."
);
if (!user) {
throw new ValidationException("User not found");
}

await this.userRepository.updateVerificationStatus(user.id);
if (user.isVerified) {
throw new ConflictException("User is already verified");
}
Comment on lines +7 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid user enumeration in verification initiation.

Same pattern: do not error on unknown or already-verified emails. Return 202/void regardless and no-op.

-    const user = await this.userRepository.findByEmail(email);
-    if (!user) {
-      throw new ValidationException("User not found");
-    }
-    if (user.isVerified) {
-      throw new ConflictException("User is already verified");
-    }
+    const user = await this.userRepository.findByEmail(email);
+    if (!user || user.isVerified) {
+      return; // No-op to prevent enumeration
+    }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async execute({ email }: { email: string }): Promise<void> {
const user = await this.userRepository.findByEmail(email);
if (!user) throw new Error("User not found");
if (user.isVerified) throw new Error("User is already verified");
const token = randomBytes(32).toString("hex");
const expires = new Date();
expires.setHours(expires.getHours() + 1);
await this.userRepository.updateVerificationToken(user.id, token, expires);
const verificationLink = `http://localhost:3000/auth/verify-email?token=${token}`;
await sendVerificationEmail(user.email, verificationLink);
}
async verifyEmail(token: string): Promise<void> {
const user = await this.userRepository.findByVerificationToken(token);
if (!user) throw new Error("Invalid or expired token");
if (
user.verificationTokenExpires &&
new Date() > user.verificationTokenExpires
) {
throw new Error(
"Token expired. Please request a new verification email."
);
if (!user) {
throw new ValidationException("User not found");
}
await this.userRepository.updateVerificationStatus(user.id);
if (user.isVerified) {
throw new ConflictException("User is already verified");
}
async execute({ email }: { email: string }): Promise<void> {
const user = await this.userRepository.findByEmail(email);
if (!user || user.isVerified) {
return; // No-op to prevent enumeration
}
// ...continue with sending verification email or other logic
}
πŸ€– Prompt for AI Agents
In src/modules/auth/use-cases/email-verification.usecase.ts around lines 7 to
14, the current implementation throws distinct errors for missing users and
already-verified users, which allows user enumeration; change the flow so the
method always returns void (202 semantics) and performs a no-op when the email
is unknown or user.isVerified is true. Specifically, remove the
ValidationException and ConflictException branches and instead early-return when
user is not found or user.isVerified, and only proceed to generate/send
verification when a non-verified user exists; keep success behavior identical
(no error) so callers receive the same response for all inputs.


// TODO: Implement email verification logic
// For now, just validate user state
}
}
Loading