shoc-backend/BACKEND_ARCHITECTURE.md
Adam Moussa c887d6d9d8 fix(security): remove hardcoded secrets from source
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.
2026-06-05 11:56:54 -04:00

19 KiB
Raw Blame History

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)

[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)

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)

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:

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:

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)

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)

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)

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)

[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)

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)

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.