# Technical documentation — shipment_document_system

Evidence date: 2026-10-04.

Source: `main` at `6f61b57` (2026-04-24, "Security Enhancements").

This document describes the tree that was inspected. It does not prescribe a target architecture.

## Repository layout

```text
shipment_document_system/
├── bitbucket-pipelines.yml
├── .gitignore
├── sql files/
│   ├── 1. CreateImportTables.sql
│   ├── 2. CreateIndexes.sql
│   ├── 3. CreateStoredProcedures.sql
│   └── 4. CreateViews.sql
└── rgbc_sds/
    ├── rgbc_sds.sln
    ├── 1. website/rgbc_sds.website/     ASP.NET Core MVC
    ├── 2. services/rgbc_sds.services/   application services
    └── 3. dal/rgbc_sds.dal/             EF Core context and entities
```

Solution file: `rgbc_sds/rgbc_sds.sln`. There is no test project in the solution.

`sonar-project.properties` and `sonarqube_scan.bat` are not in git. The pipeline file is the SonarQube entry point that is tracked.

## Runtime

| Item | Evidence |
|---|---|
| Language | C# |
| Framework | ASP.NET Core MVC, `Microsoft.NET.Sdk.Web` |
| Target | `net8.0` on website, services, and DAL |
| Nullable | enabled |
| Implicit usings | enabled |
| Razor runtime compilation | `Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation` 7.0.18 |
| Local HTTP URL | `http://localhost:5010` (`Properties/launchSettings.json`, profile `http`) |
| Local HTTPS URL | `https://localhost:7192` and `http://localhost:5010` (profile `https`) |
| IIS Express HTTP | `http://localhost:62266` |
| IIS Express SSL port | 44396 |
| IIS Express auth | `windowsAuthentication: false`, `anonymousAuthentication: true` |

Hosting in production (IIS, Windows auth, Kestrel, a reverse proxy) is **Unknown / Requires Confirmation**. No Dockerfile, publish profile, or deployment YAML is tracked. `.gitignore` ignores `Properties/PublishProfiles`.

## Projects and references

`rgbc_sds.website` references `rgbc_sds.services` and `rgbc_sds.dal`.

`rgbc_sds.services` references `rgbc_sds.dal`.

Packages that matter:

| Package | Project | Version |
|---|---|---|
| Microsoft.EntityFrameworkCore.SqlServer | website | 7.0.20 |
| Microsoft.EntityFrameworkCore.SqlServer | dal | 7.0.18 |
| Microsoft.EntityFrameworkCore.Tools / Design | website and dal | 7.0.18–7.0.20 |
| EPPlus | website and services | 7.0.9 |
| Microsoft.Graph | services | 4.54.0 |
| Azure.Identity | services | 1.11.4 |
| Newtonsoft.Json | website and services | 13.0.3 |
| System.DirectoryServices | website | 8.0.0 |
| Microsoft.Data.SqlClient | dal | 5.2.0 |
| Microsoft.AspNetCore.Http | services | 2.2.2 |
| Microsoft.AspNetCore.Http.Features | services | 3.1.32 |

EF Core 7 on a `net8.0` app is what the project files pin. No `global.json` was found.

## Composition root

`rgbc_sds.website/Program.cs`:

1. `SecureConfig.LoadEnvironmentVariables()` reads the first `.env` that exists. Candidates: `{cwd}/../.env`, then four levels above `AppContext.BaseDirectory` (the comment says this covers `bin/Debug/net8.0`). Existing process environment variables are not overwritten. Lines that are empty or start with `#` are skipped.
2. Requires `RGBC_SDS_CONNECTION_STRING`.
3. Requires configuration section `SharepointCredentials` keys `TENANT_ID`, `CLIENT_ID`, `CLIENT_SECRET`, `SCOPE`, `SITE_ID`.
4. `AddDbContext<rgbc_sdsContext>` with `UseSqlServer`.
5. Clears logging providers, then adds console and debug.
6. Scoped services: `UserService`, `ShipmentService`, `SupplierService`, `SharePointService`, `IImportService` → `ImportService`.
7. Controllers with views, Razor Pages with runtime compilation, session, CORS policy `CorsPolicy`.
8. Pipeline: developer exception page or `/Home/Error` plus HSTS, HTTPS redirection, static files, routing, authorisation, Razor Pages, session, conventional route `account/login`, `MapControllers`.

`AddSession` is invoked twice. The first sets idle timeout 10 minutes, HttpOnly, and `IsEssential`. The second passes no options. Which registration wins was not executed.

`UseSession` is registered after `UseAuthorization` and after `MapRazorPages`. Endpoint order follows the source. Session is available to the MVC actions that read `HttpContext.Session` in this codebase; those actions are controller actions mapped afterwards.

`appsettings.json` contains logging levels and `"AllowedHosts": "*"`. `appsettings.Development.json` was not opened for secrets; the security commit removed the credential block from the main appsettings file.

`.env.example` (not loaded by itself; it is a template):

- `RGBC_SDS_CONNECTION_STRING`
- `SharepointCredentials__TENANT_ID`
- `SharepointCredentials__CLIENT_ID`
- `SharepointCredentials__CLIENT_SECRET`
- `SharepointCredentials__SCOPE` example `https://graph.microsoft.com/.default`
- `SharepointCredentials__SITE_ID`

`SharePointService` reads the keys as `SharePointCredentials:*` (capital P). Configuration key matching in ASP.NET Core is case-insensitive. Do not commit a filled `.env`. `.gitignore` does not list `.env`. **Requires Confirmation** that real `.env` files are kept out of git by process rather than by ignore rules.

## Request map

Attribute routes that exist on controllers:

| Method | Path | Action |
|---|---|---|
| GET | `/`, `account`, `account/login` | `AccountController.Login` |
| POST | `account/login` | `AccountController.Login` |
| GET | `account/logout` | `AccountController.Logout` |
| GET | `shipment/shipmentlist` | list |
| GET | `shipment/shipmentdetails/{id}` | detail |
| POST | `shipment/addnewshipment/` | empty add form |
| POST | `shipment/addshipment` | insert |
| POST | `shipment/saveshipment` | insert when `Id == 0`, otherwise update |
| POST | `shipment/updateshipment` | update |
| POST | `shipment/submitshipment` | status `SUBMITTED` |
| POST | `shipment/cancelshipment` | status `CANCELLED` |
| POST | `shipment/deletefile` | clear one document |
| GET | `shipment/filtershipmentlist` | filter |
| GET | `shipment/exportshipments` | xlsx |
| GET | `shipment/import`, `shipment/importshipments` | redirect to import upload |
| GET/POST | `import/upload` | form and session create. POST has anti-forgery |
| GET | `import/preview/{sessionId}` | preview, query `page`, `pageSize` (default 50) |
| GET | `import/errors/{sessionId}` | errors |
| GET/POST | `import/execute/{sessionId}` | confirm page and run. POST has anti-forgery |
| GET | `import/results/{sessionId}` | results |
| POST | `import/cancel/{sessionId}` | cancel. Anti-forgery |
| GET | `import/history` | last 20 for the session user name |
| GET | `import/downloadtemplate` | xlsx template |
| GET | `supplier/supplierlist` | list |
| GET | `supplier/filtersupplierlist` | filter |
| GET | `supplier/getsuppliertoupdate/{id}` | edit form |
| GET | `supplier/addsupplier` | add form |
| POST | `supplier/savesupplier` | insert |
| POST | `supplier/updatesupplier` | update |
| POST | `supplier/deletesupplier/{id}` | soft delete |

`UserController`, `HomeController`, and `SharePointController` actions do not carry `[Route]`. The `{controller}/{action}/{id?}` pattern is commented out. Those actions are not in the table above because this file cannot show a URL that `Program.cs` does not register. Confirm by running the site before linking to them.

Anti-forgery tokens are on the import upload, execute, and cancel posts, and are rendered on several shipment and supplier forms. Not every post action has `[ValidateAntiForgeryToken]`. The shipment save actions do not.

## Data

Database: SQL Server via EF Core. Connection string only from the environment. The context constructor takes `DbContextOptions` and does not embed a connection string.

EF Power Tools config `efpt.config.json` lists generated tables:

- `[dbo].[mstUser]`
- `[dbo].[sdsShipment]`
- `[dbo].[sdsSupplier]`

`IncludeConnectionString` is false. `UseDatabaseNames` is true.

Import entities are hand-written and mapped with `[Table]`:

| Class | Table |
|---|---|
| `ImportSession` | `sdsImportSessions` |
| `StagingShipment` | `sdsStagingShipments` |
| `ImportErrorLog` | `sdsImportErrorLogs` |

`sdsShipment.supplier_id` → `sdsSupplier.id`, constraint name `FK_sdsShipment_sdsSupplier`, delete behaviour `ClientSetNull`.

`mstUser` columns include `strDomainLogin` (max 100), `strUserName` (max 50), `strPassword` (max 32), `strEmail`, `refUserGroupSDSID` (default SQL 0), and other `refUserGroup*` columns this application does not query. **Inferred:** shared user table. **Requires Confirmation.**

Shipment and supplier string columns used by the screen are generally `nvarchar` max 255 in the fluent model. `shipment_status` max 50. `cancel_reason` max 255. Date columns `eta`, `container_received`, `redirect_to_SACD`, `created_at`, `updated_at` are `datetime`.

No EF Migrations folder was found. Schema for the original three tables is whatever the database already had when Power Tools ran. Import schema is the SQL scripts plus the fluent configuration in `rgbc_sdsContext`. Applying the scripts is a manual step described by comments inside the scripts ("Run this script FIRST" through "FOURTH"). The application does not call `Database.Migrate`.

Stored procedures and views in `sql files/` are not referenced from C#.

## SharePoint integration

`SharePointService`:

- Builds `IConfidentialClientApplication` with client id, client secret, and tenant authority.
- `AcquireTokenForClient` using the configured scope.
- `GraphServiceClient` (Microsoft.Graph 4.x) with a bearer header.
- Upload: `Sites[siteId].Drive.Root.ItemWithPath("{folder}/{file}").Content.PutAsync`.
- Delete: get by path, then delete by item id. HTTP 404 is ignored.
- Preview: `.Preview().Request().PostAsync()`.
- Rename: folder rename and file rename helpers used when an indent changes.

`UploadFilesAsync` deletes the previous object and uploads the new one only inside the branch where the entity's filename column is already non-empty. `ShipmentController.FileHelper` assigns the new name before that call, so the branch is taken for a newly chosen file as well. A `ServiceException` with status 404 around the whole switch is caught and ignored for that file.

`FileUploadService` is a separate Graph helper (list sites, token via `ClientSecretCredential`). It is constructed in `ShipmentController` and has no call site.

## Authentication model

There is no cookie authentication scheme, no JWT, and no ASP.NET Identity store despite `using Microsoft.AspNetCore.Identity` in `AccountController` (the import is unused).

Application state after login is three session keys:

- `LoggedInUsername` ← `mstUser.strUserName`
- `LoggedInUserRole` ← `ADMIN` / `VIEWER` / `LOGISTICS`
- `LoggedInUserId` ← `mstUser.UserID`

Session cookie: HttpOnly and essential on the first `AddSession` call. `Secure` is not set in code.

The login button does not collect credentials. If the process is not running under Windows authentication, `User.Identity.Name` is empty and the lookup uses that empty string. Local IIS Express is configured with anonymous authentication.

## Background work, queues, storage, monitoring

- No `IHostedService`, Hangfire, or queue client.
- `CleanupOldImportSessionsAsync` exists and is not scheduled.
- File storage is SharePoint, not the local disk. `wwwroot` holds CSS, JS, the logo, and vendor libraries.
- Logging is console and debug. A few import paths also use `Console.WriteLine` with the prefix `DEBUG:`.
- Monitoring in-repo is the SonarQube pipeline. No application insights, health check, or metrics endpoint was found.
- `Home/Error` returns `ErrorViewModel` with `Activity.Current.Id` or `HttpContext.TraceIdentifier`. The view is the standard scaffold.

## Security notes from the code

These are observations, not a penetration test.

- Startup fails closed when SQL or SharePoint settings are missing (`6f61b57`).
- SharePoint secret is no longer in `appsettings.json`.
- Login does not verify `strPassword`.
- Unknown users are not rejected, because `ValidateUser` does not return null (see business rule BR-002).
- Role checks are in Razor, not on the actions.
- CORS policy allows any origin if it is ever attached. `UseCors` was not found.
- `AllowedHosts` is `*`.
- EPPlus non-commercial licence context is hard-coded.
- Pipeline tokens are `${SONAR_TOKEN}` and `${SONAR_HOST_URL}`, not literals.

## Frontend

Server-rendered Razor. Layout loads Bootstrap from `wwwroot`, and jQuery, jQuery UI, inputmask, and Font Awesome from CDNs, with `integrity` attributes on those CDN tags.

`wwwroot/js/site.js` and `wwwroot/js/customJS.js` are referenced. Page scripts in the shipment views set the logout label and wire datepickers and the cancel modal.

No SPA framework, no TypeScript, no bundler config.

## Pipeline and local scan

`bitbucket-pipelines.yml`:

- Image `mcr.microsoft.com/dotnet/sdk:7.0`.
- Branch filter `{main}` only.
- Installs OpenJDK 17 JRE and the global `dotnet-sonarscanner` tool.
- `dotnet sonarscanner begin` with key `overdrive_shipment_document_system_dccdf710-08f7-47e7-bc1c-d390c06b0c7f`.
- `dotnet build **/rgbc_sds.sln`.
- `dotnet sonarscanner end`.
- Caches: `dotnetcore`, `~/.sonar`.

The SDK image is major version 7 and the projects target `net8.0`. Whether that build succeeds on the pipeline is **Unknown / Requires Confirmation**.

No unit or UI test project is in the solution. No test command is in the pipeline.

## Development process visible in the repo

- Git on Bitbucket, default branch `main`.
- Commits are short subject lines, often one feature or fix at a time. No pull-request template is in the tree.
- `.gitignore` ignores Visual Studio folders, `bin` and `obj` for `net7.0` and `net8.0`, and publish profiles.
- EF Power Tools generated the original entities (`// <auto-generated>` header on `mstUser`, `sdsShipment`, `sdsSupplier`).

## Open technical questions

1. Production host, URL, and whether Windows authentication is enabled there.
2. SQL Server instance, database name, and whether `sql files/` have been applied.
3. Whether the .NET 7 pipeline image can build the `net8.0` solution.
4. Whether `.env` is excluded from developer machines' commits, given it is not in `.gitignore`.
5. Whether `/Home/Error` is reachable without a conventional route.
6. Whether the second `AddSession()` call drops the 10-minute timeout.
7. Intended shipment status vocabulary (`SUBMITTED` versus `COMPLETE` / `COMPLETED`).
8. Intended import mode behaviour versus the upsert loop.
9. EPPlus licence (code sets non-commercial).
10. Ownership of `mstUser` and the meaning of the other `refUserGroup*` columns.
