mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 17:43:12 +00:00
Replace all hardcoded credentials with configuration-injected values:
- SQL Server connection strings -> ${CONNECTION_STRING} env-var placeholders (4 appsettings files)
- SendGrid API keys -> ${SENDGRID_API_KEY} (incl. commented copies in SendMessage.cs)
- JWT signing secret -> ${JWT_SECRET} (3 appsettings files)
- AWS access key pair in UploadFileHp.cs -> DI-injected IAmazonS3 (SDK default credential chain)
- Google Maps API keys in App.razor / Home.razor -> IConfiguration lookup
- Legacy SMTP credentials in SendMessage.cs comments -> placeholders
Add .env.example documenting required environment variables and a
Configuration & Secrets section in BACKEND_ARCHITECTURE.md.
All exposed credentials were rotated 2026-06-05 prior to this scrub.
Source: github-audit-report.md Criticals 1-2 (Agent A4).
Verified: dotnet build 0 errors; secret-pattern grep clean.
564 lines
19 KiB
Markdown
564 lines
19 KiB
Markdown
# Backend Architecture Guide
|
||
|
||
## 🏗️ Clean Architecture Overview
|
||
|
||
The backend follows **Clean Architecture** with strict layer separation:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ API LAYER (Controllers) │
|
||
│ - HTTP Request/Response │
|
||
│ - Authentication & Authorization │
|
||
│ - Exception Handling │
|
||
│ - Input Validation (basic) │
|
||
└─────────────────┬───────────────────────────────────────────┘
|
||
│ Calls
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SERVICE LAYER (Business Logic) │
|
||
│ - Business Rules │
|
||
│ - FluentValidation (detailed validation) │
|
||
│ - Data Transformation │
|
||
│ - Orchestration │
|
||
└─────────────────┬───────────────────────────────────────────┘
|
||
│ Calls
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DATA SERVICE LAYER (Persistence) │
|
||
│ - Database Access │
|
||
│ - Entity Operations (CRUD) │
|
||
│ - Query Building │
|
||
│ - Transaction Management │
|
||
└─────────────────┬───────────────────────────────────────────┘
|
||
│ Uses
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DATA LAYER (EF Core) │
|
||
│ - DbContext │
|
||
│ - Entity Models │
|
||
│ - Database Schema │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 📁 Project Structure
|
||
|
||
```
|
||
backend/shoc-backend/
|
||
│
|
||
├── Api.SeaHavenIndustries/ # 🌐 API Layer
|
||
│ ├── Controllers/ # HTTP endpoints
|
||
│ ├── DTOs/ # Data Transfer Objects (API contracts)
|
||
│ ├── Program.cs # App startup & DI registration
|
||
│ └── appsettings.json # Configuration
|
||
│
|
||
├── SeaHaven.Services/ # 💼 Service Layer (Business Logic)
|
||
│ ├── Interfaces/ # Service contracts (IContactService, etc.)
|
||
│ ├── Implementation/ # Service implementations
|
||
│ ├── DTOs/ # Internal DTOs for service layer
|
||
│ └── Validators/ # FluentValidation validators
|
||
│
|
||
├── SeaHaven.DataServices/ # 💾 Data Access Layer
|
||
│ ├── Interfaces/ # DataService contracts (IContactDataService, etc.)
|
||
│ ├── Implementation/ # DataService implementations
|
||
│ └── DTOs/ # Data layer DTOs
|
||
│
|
||
└── Data.SeaHavenIndustries/ # 🗄️ Data Layer (EF Core)
|
||
├── Models/ # Entity models (Contact, Employee, etc.)
|
||
├── ApplicationDbContext.cs # EF Core DbContext
|
||
└── Migrations/ # Database migrations
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 Data Flow Example (Creating a Contact)
|
||
|
||
### 1️⃣ **API Layer** (`ContactController.cs`)
|
||
```csharp
|
||
[HttpPost("Create")]
|
||
public async Task<IActionResult> AddContacts(Contact_DTO dto)
|
||
{
|
||
try
|
||
{
|
||
// Get authenticated user ID
|
||
var userId = _userManager.GetUserId(User);
|
||
if (userId == null)
|
||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||
|
||
// Convert API DTO → Service DTO
|
||
var serviceDto = dto.ToServiceCreateDTO();
|
||
|
||
// Call Service Layer
|
||
await _contactService.CreateContactAsync(serviceDto, userId);
|
||
|
||
return Ok(new Response { Status = "Success", Message = "Contact created" });
|
||
}
|
||
catch (ValidationException vex)
|
||
{
|
||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||
}
|
||
catch (KeyNotFoundException)
|
||
{
|
||
return NotFound(new Response { Status = "Error", Message = "Resource not found" });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
```
|
||
|
||
**Responsibilities:**
|
||
- ✅ Authenticate user
|
||
- ✅ Extract user ID from claims
|
||
- ✅ Map API DTO to Service DTO
|
||
- ✅ Catch exceptions and return proper HTTP status codes
|
||
- ❌ NO business logic
|
||
- ❌ NO database access
|
||
|
||
---
|
||
|
||
### 2️⃣ **Service Layer** (`ContactService.cs`)
|
||
|
||
```csharp
|
||
public class ContactService : IContactService
|
||
{
|
||
private readonly IContactDataService _dataService;
|
||
private readonly IValidator<CreateContactDTO> _createValidator;
|
||
|
||
public ContactService(
|
||
IContactDataService dataService,
|
||
IValidator<CreateContactDTO> createValidator)
|
||
{
|
||
_dataService = dataService;
|
||
_createValidator = createValidator;
|
||
}
|
||
|
||
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
|
||
{
|
||
// STEP 1: Validate using FluentValidation
|
||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||
if (!validationResult.IsValid)
|
||
{
|
||
throw new ValidationException(validationResult.Errors);
|
||
}
|
||
|
||
// STEP 2: Business logic (if any)
|
||
// Example: Check for duplicates, apply business rules, etc.
|
||
|
||
// STEP 3: Map Service DTO → Data DTO
|
||
var dataDto = new CreateContactDataDTO
|
||
{
|
||
FirstName = dto.FirstName,
|
||
LastName = dto.LastName,
|
||
Email = dto.Email,
|
||
Phone = dto.Phone
|
||
};
|
||
|
||
// STEP 4: Call Data Service Layer
|
||
var createdContact = await _dataService.CreateAsync(dataDto, userId);
|
||
|
||
// STEP 5: Map Data DTO → Service DTO and return
|
||
return new ContactDTO
|
||
{
|
||
Id = createdContact.Id,
|
||
FirstName = createdContact.FirstName,
|
||
LastName = createdContact.LastName,
|
||
Email = createdContact.Email,
|
||
Phone = createdContact.Phone
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
**Responsibilities:**
|
||
- ✅ Validate input using FluentValidation
|
||
- ✅ Apply business rules
|
||
- ✅ Orchestrate operations
|
||
- ✅ Transform data between layers
|
||
- ❌ NO HTTP concerns
|
||
- ❌ NO direct database access
|
||
|
||
---
|
||
|
||
### 3️⃣ **Data Service Layer** (`ContactDataService.cs`)
|
||
|
||
```csharp
|
||
public class ContactDataService : IContactDataService
|
||
{
|
||
private readonly ApplicationDbContext _db;
|
||
|
||
public ContactDataService(ApplicationDbContext db)
|
||
{
|
||
_db = db;
|
||
}
|
||
|
||
public async Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId)
|
||
{
|
||
// STEP 1: Create entity from DTO
|
||
var contact = new Contact
|
||
{
|
||
FirstName = dto.FirstName,
|
||
LastName = dto.LastName,
|
||
Email = dto.Email,
|
||
Phone = dto.Phone,
|
||
CreatedBy = userId,
|
||
CreatedDate = DateTime.UtcNow,
|
||
ModifiedBy = userId,
|
||
ModifiedDate = DateTime.UtcNow
|
||
};
|
||
|
||
// STEP 2: Add to database
|
||
_db.Contacts.Add(contact);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// STEP 3: Map entity → Data DTO and return
|
||
return new ContactDataDTO
|
||
{
|
||
Id = contact.Id,
|
||
FirstName = contact.FirstName,
|
||
LastName = contact.LastName,
|
||
Email = contact.Email,
|
||
Phone = contact.Phone
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
**Responsibilities:**
|
||
- ✅ Direct database operations (CRUD)
|
||
- ✅ Entity mapping
|
||
- ✅ Set audit fields (CreatedBy, CreatedDate, etc.)
|
||
- ✅ Transaction management
|
||
- ❌ NO validation
|
||
- ❌ NO business logic
|
||
|
||
---
|
||
|
||
## 🔌 Dependency Injection (DI)
|
||
|
||
### Automatic Assembly Scanning
|
||
|
||
Services are **automatically registered** by scanning assemblies using naming convention.
|
||
|
||
**In `Program.cs`:**
|
||
```csharp
|
||
builder.Services.AddDataServices(); // Auto-register all Data Services
|
||
builder.Services.AddBusinessServices(); // Auto-register all Business Services + Validators
|
||
```
|
||
|
||
### How It Works
|
||
|
||
**Module Files:**
|
||
- `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
|
||
- `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
|
||
|
||
**Process:**
|
||
1. Scans assembly for all classes
|
||
2. Finds interface matching pattern `I{ClassName}`
|
||
3. Registers as Scoped service
|
||
4. Also auto-registers FluentValidation validators
|
||
|
||
### Naming Convention Required
|
||
|
||
| Interface | Implementation | Registered? |
|
||
|-----------|---------------|-------------|
|
||
| `IContactService` | `ContactService` | ✅ YES |
|
||
| `IContactDataService` | `ContactDataService` | ✅ YES |
|
||
| `ISomething` | `SomethingDifferent` | ❌ NO |
|
||
|
||
**Rule**: Interface must be `I` + Class name exactly
|
||
|
||
### Usage in Code
|
||
|
||
Once registered, inject via constructor:
|
||
|
||
```csharp
|
||
public class ContactController : Controller
|
||
{
|
||
private readonly IContactService _contactService;
|
||
|
||
public ContactController(IContactService contactService)
|
||
{
|
||
_contactService = contactService;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Benefits
|
||
|
||
✅ No manual registration - just follow naming convention
|
||
✅ Auto-discovery - new services work automatically
|
||
✅ FluentValidation auto-registered
|
||
|
||
---
|
||
|
||
## ✅ FluentValidation
|
||
|
||
### Validator Example (`CreateContactDTOValidator.cs`)
|
||
|
||
```csharp
|
||
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
|
||
{
|
||
public CreateContactDTOValidator()
|
||
{
|
||
RuleFor(x => x.FirstName)
|
||
.NotEmpty().WithMessage("First name is required")
|
||
.MaximumLength(50).WithMessage("First name cannot exceed 50 characters");
|
||
|
||
RuleFor(x => x.LastName)
|
||
.NotEmpty().WithMessage("Last name is required")
|
||
.MaximumLength(50).WithMessage("Last name cannot exceed 50 characters");
|
||
|
||
RuleFor(x => x.Email)
|
||
.NotEmpty().WithMessage("Email is required")
|
||
.EmailAddress().WithMessage("Invalid email format")
|
||
.MaximumLength(100).WithMessage("Email cannot exceed 100 characters");
|
||
|
||
RuleFor(x => x.Phone)
|
||
.Matches(@"^\+?[1-9]\d{1,14}$").When(x => !string.IsNullOrEmpty(x.Phone))
|
||
.WithMessage("Invalid phone number format");
|
||
}
|
||
}
|
||
```
|
||
|
||
### Where Validation Happens
|
||
|
||
```
|
||
❌ Controller → NO detailed validation (only catches ValidationException)
|
||
✅ Service → YES - FluentValidation runs here
|
||
❌ DataService → NO validation
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 Interface Examples
|
||
|
||
### Service Interface (`IContactService.cs`)
|
||
```csharp
|
||
public interface IContactService
|
||
{
|
||
Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId);
|
||
Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId);
|
||
Task DeleteContactAsync(int id, string userId);
|
||
Task<ContactDTO> GetContactByIdAsync(int id);
|
||
Task<PagedResult<ContactDTO>> GetContactsPagedAsync(int page, int pageSize, string? search);
|
||
}
|
||
```
|
||
|
||
### Data Service Interface (`IContactDataService.cs`)
|
||
```csharp
|
||
public interface IContactDataService
|
||
{
|
||
Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId);
|
||
Task<ContactDataDTO> UpdateAsync(int id, UpdateContactDataDTO dto, string userId);
|
||
Task DeleteAsync(int id, string userId);
|
||
Task<ContactDataDTO?> GetByIdAsync(int id);
|
||
Task<PagedResult<ContactDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📦 DTO Types
|
||
|
||
### 1. **API DTOs** (`Api.SeaHavenIndustries/DTOs/`)
|
||
- Used for HTTP requests/responses
|
||
- Map to frontend JSON structure
|
||
- Example: `Contact_DTO`, `EditContact_DTO`
|
||
|
||
### 2. **Service DTOs** (`SeaHaven.Services/DTOs/`)
|
||
- Used between Controller ↔ Service
|
||
- Business-focused structure
|
||
- Example: `CreateContactDTO`, `UpdateContactDTO`, `ContactDTO`
|
||
|
||
### 3. **Data DTOs** (`SeaHaven.DataServices/DTOs/`)
|
||
- Used between Service ↔ DataService
|
||
- Database-focused structure
|
||
- Example: `CreateContactDataDTO`, `ContactDataDTO`
|
||
|
||
### DTO Mapping Flow
|
||
```
|
||
API DTO → Service DTO → Data DTO → Entity
|
||
(HTTP) (Business) (Data) (Database)
|
||
```
|
||
|
||
---
|
||
|
||
## 🚨 Exception Handling Pattern
|
||
|
||
### Controller Exception Handling (STANDARD PATTERN)
|
||
|
||
```csharp
|
||
[HttpPost("Create")]
|
||
public async Task<IActionResult> Create([FromBody] SomeDTO dto)
|
||
{
|
||
try
|
||
{
|
||
// Get user ID
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
if (userId == null)
|
||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||
|
||
// Call service
|
||
await _service.CreateAsync(dto, userId);
|
||
|
||
return Ok(new Response { Status = "Success", Message = "Created successfully" });
|
||
}
|
||
catch (ValidationException vex) // ⚠️ CATCH THIS FIRST
|
||
{
|
||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||
}
|
||
catch (KeyNotFoundException) // ⚠️ THEN THIS
|
||
{
|
||
return NotFound(new Response { Status = "Error", Message = "Not found" });
|
||
}
|
||
catch (Exception ex) // ⚠️ GENERIC LAST
|
||
{
|
||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
```
|
||
|
||
### HTTP Status Code Mapping
|
||
- **200 OK** → Success
|
||
- **400 Bad Request** → Validation errors
|
||
- **401 Unauthorized** → Not authenticated
|
||
- **404 Not Found** → Resource not found
|
||
- **500 Internal Server Error** → Unexpected errors
|
||
|
||
---
|
||
|
||
## 🔐 Authentication & User ID
|
||
|
||
### Getting User ID (Two Methods)
|
||
|
||
**Method 1: Using Claims (Preferred)**
|
||
```csharp
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
if (userId == null)
|
||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||
```
|
||
|
||
**Method 2: Using UserManager (for Identity)**
|
||
```csharp
|
||
var userId = _userManager.GetUserId(User);
|
||
if (userId == null)
|
||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||
```
|
||
|
||
### Audit Fields
|
||
Every entity should track:
|
||
- `CreatedBy` (string - User ID GUID)
|
||
- `CreatedDate` (DateTime)
|
||
- `ModifiedBy` (string - User ID GUID)
|
||
- `ModifiedDate` (DateTime)
|
||
|
||
---
|
||
|
||
## 📋 Naming Conventions
|
||
|
||
### Controllers
|
||
- Named after entity + "Controller"
|
||
- Example: `ContactController`, `EmployeeController`
|
||
|
||
### Services
|
||
- Interface: `I[Entity]Service`
|
||
- Implementation: `[Entity]Service`
|
||
- Example: `IContactService`, `ContactService`
|
||
|
||
### Data Services
|
||
- Interface: `I[Entity]DataService`
|
||
- Implementation: `[Entity]DataService`
|
||
- Example: `IContactDataService`, `ContactDataService`
|
||
|
||
### DTOs
|
||
- API Layer: `[Entity]_DTO`, `Edit[Entity]_DTO`
|
||
- Service Layer: `Create[Entity]DTO`, `Update[Entity]DTO`, `[Entity]DTO`
|
||
- Data Layer: `Create[Entity]DataDTO`, `[Entity]DataDTO`
|
||
|
||
---
|
||
|
||
## ✅ Best Practices Checklist
|
||
|
||
### Controllers
|
||
- [ ] Use dependency injection for services
|
||
- [ ] Get user ID from `User.FindFirstValue(ClaimTypes.NameIdentifier)`
|
||
- [ ] Return 401 if userId is null
|
||
- [ ] Catch `ValidationException` BEFORE generic `Exception`
|
||
- [ ] Return appropriate HTTP status codes
|
||
- [ ] NO business logic in controllers
|
||
- [ ] NO direct database access (`_db`)
|
||
|
||
### Services
|
||
- [ ] Validate using FluentValidation
|
||
- [ ] Throw `ValidationException` for validation errors
|
||
- [ ] Throw `KeyNotFoundException` for not found
|
||
- [ ] Pass userId to DataService
|
||
- [ ] Transform DTOs between layers
|
||
- [ ] Implement business rules here
|
||
|
||
### Data Services
|
||
- [ ] Direct database access only
|
||
- [ ] Set audit fields (CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
|
||
- [ ] Use async/await for all database operations
|
||
- [ ] NO validation logic
|
||
- [ ] NO business logic
|
||
|
||
---
|
||
|
||
## 🎓 Quick Reference
|
||
|
||
| Layer | Purpose | Can Access | Cannot Access |
|
||
|-------|---------|-----------|---------------|
|
||
| **Controller** | HTTP handling, auth | Service layer | Database directly |
|
||
| **Service** | Business logic, validation | DataService layer | Database directly |
|
||
| **DataService** | Database operations | DbContext, Entities | HTTP context |
|
||
| **Data** | Entity definitions | Nothing (POCOs) | Application logic |
|
||
|
||
---
|
||
|
||
## 📚 Example: Full CRUD Implementation
|
||
|
||
See these files for complete examples:
|
||
- **Contact**: `ContactController.cs`, `ContactService.cs`, `ContactDataService.cs`
|
||
- **Employee**: `EmployeeController.cs`, `EmployeeService.cs`, `EmployeeDataService.cs`
|
||
- **Asset**: `AssetController.cs`, `AssetService.cs`, `AssetDataService.cs`
|
||
|
||
All follow the same Clean Architecture pattern!
|
||
|
||
---
|
||
|
||
## 🔐 Configuration & Secrets
|
||
|
||
No secrets are stored in source. The committed `appsettings*.json` files hold only
|
||
`${PLACEHOLDER}` tokens; real values must be supplied at runtime through one of the
|
||
standard .NET configuration sources (later sources win):
|
||
|
||
1. `appsettings.json` / `appsettings.{Environment}.json` — committed, placeholders only.
|
||
2. **User Secrets** (local dev): `dotnet user-secrets set "Section:Key" "value"`.
|
||
3. **Environment variables** — use `__` (double underscore) as the section separator.
|
||
In AWS Elastic Beanstalk these are the environment properties.
|
||
|
||
See `.env.example` in the repo root for the full list. Required values:
|
||
|
||
| Setting | Env-var form | Used by | Notes |
|
||
|---|---|---|---|
|
||
| `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` | Both projects | SQL Server connection string |
|
||
| `SendGrid:ApiKey` | `SendGrid__ApiKey` | Both projects | Transactional email |
|
||
| `JWT:Secret` | `JWT__Secret` | `Api.SeaHavenIndustries` | JWT signing key |
|
||
| `GoogleMaps:ApiKey` | `GoogleMaps__ApiKey` | `SeaHavenIndustries` (Blazor) | Maps/Places; read in `App.razor` + `Home.razor` |
|
||
|
||
### AWS credentials
|
||
|
||
`UploadFileHp` (S3 uploads) uses the **AWS SDK default credential chain** — it no longer
|
||
hardcodes access keys. Credentials resolve from `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
|
||
env vars, the shared profile/SSO, or (preferred in production) the EC2/ECS instance role.
|
||
The region comes from `AddDefaultAWSOptions` in `Program.cs`.
|
||
|
||
> ⚠️ The secrets previously committed to this repo (DB password, SendGrid key, JWT secret,
|
||
> Google Maps keys, AWS access key) should be considered compromised and **rotated**.
|
||
|