Architecture — External Services¶
Everything the application talks to beyond its own databases, and the three separate mechanisms it uses to run work in the background.
Outbound integrations¶
flowchart TD
APP["NKO ERP web application"]
APP --> NET["Network calls"]
subgraph EXT["Third party over the network"]
direction TB
S3["AWS S3<br/>document storage"]
FX["data.fixer.io<br/>never called"]
SMTP["SMTP server<br/>notification e-mail"]
end
NET --> EXT
APP --> LOC["Local process"]
LOC --> WK["wkhtmltopdf via Rotativa<br/>PDF rendering"]
APP --> DATA["SQL Server"]
subgraph DBS["Databases"]
direction TB
DB1[("NKO_DB")]
DB2[("NKO_DB_Warehouse")]
DB3[("Hangfire job store")]
end
DATA --> DBS
AWS S3 — document storage¶
Every uploaded document — employee attachments, contracts, procurement files, e-signatures — is
stored in S3, not on disk or in the database. The abstraction is IS3ServiceHandler
(S3ServiceHandler), registered as transient and configured from StorageConfigs:
| Setting | Source |
|---|---|
region |
appsettings. → Storage |
client / client |
appsettings. → Storage |
Files are addressed by storage key, and the application proxies downloads through its own endpoints rather than issuing pre-signed URLs.
Unauthenticated file access — R3-1
PublicFilesController exposes GET /api/pubattachments/{id} and
GET /api/viewpubattachments/{id}, which pass id straight to DownloadFile with no
authentication and no ownership check. The sibling FilesController requires a login but its
ownership check is commented out. See Recommendations.
data.fixer.io — present in the code, never called¶
BExchangeRate.GetExchangeRate(date) would fetch rates from:
but nothing in the application calls it — the only BExchangeRate methods in use are Get and
GetAll. So there is no outbound traffic to fixer.io. What remains is the API key, a hard-coded
const committed to the repository (R7-17).
This matters more than it looks, because it means the global exchange rate (ExchangeRates) has
no update path at all: Create and Update are unreachable too. That rate is what purchase requests
use to value TL and EUR amounts in USD — and so to decide which segregation-of-duties sign-offs a PR
needs (R7-1). Payroll allocation and purchase-order lines use per-donor rates
(DonorExchangeRate), entered by hand under Administration → Donors.
SMTP — notification e-mail¶
EmailManager (NKO.Lib/NKO.Shared/Helpers/EmailManager.cs) sends approval notifications, security
alerts and birthday greetings using System.Net.Mail.SmtpClient on port 587 with SSL. The host is
hard-coded, and so are the credentials — SiteSettings.SystemEmail / SystemEmailPass are static
string literals compiled into NKO.Lib/NKO.Shared/Helpers/SiteSettings.cs, not configuration.
Mailbox passwords are committed to source control — R1-9
SiteSettings.cs hard-codes SystemEmailPass and AutoReplyEmailPass as plain string
literals, plus a superseded SystemEmailPass left in a comment. Every URL, the HR mailbox and
the template path are compiled in the same way. See Recommendations.
Two conditions are checked before any SMTP connection is made, and both fail silently:
- The e-mail switch. Both send methods read
CommonSettingrow 6 (EnableEmailService, set in General Settings) and send only if it parses astrue. On a new database the row does not exist, so no mail is sent at all (R5-5). - The recipient must be an enabled user (notification path only).
SendEmail(subject, body, link, to)looks up anAppUserwhoseEmailequals the recipient address and sends only if that employee's login is enabled. Mail to an address with no matching employee, or to an employee whose login is disabled, is dropped.
MailKit and MimeKit are referenced and imported at the top of the same file but never used —
the working code is all System.Net.Mail.
Exceptions are swallowed
Both send paths wrap the call in try { … } catch (Exception ex) { } with an empty body, so a
mail failure is invisible — no log, no retry, no indication to the caller.
wkhtmltopdf via Rotativa¶
PDF export (purchase orders, payment orders, employee cards, training requests) is rendered by
Rotativa.AspNetCore 1.2.0, which shells out to a local wkhtmltopdf binary in
wwwroot/Rotativa, configured at startup:
Rotativa fetches the page's header and footer over HTTP from
/Document/ExportationRotativeHeader and /Document/ExportationRotativeFooter, which is why those
two actions are [AllowAnonymous] — the renderer has no user session.
Background work — three schedulers¶
The application runs scheduled and deferred work through three independent mechanisms. They do not share configuration, storage or a dashboard.
| Mechanism | Registered in | Storage | What runs on it |
|---|---|---|---|
| Quartz.NET 3.6.2 | Program. |
in-memory | Update (cron 0 0 3 ? * * * — daily 03:00); Export |
| Hangfire 1.7.24 | Startup. |
SQL Server (Hangfire) |
Fire-and-forget warehouse exports via Background |
Custom IHosted |
Add |
none | Birthday, using Cronos for cron parsing |
Quartz¶
var jobKey = new JobKey(nameof(ExportHistoryJob));
q.AddJob<ExportHistoryJob>(opts => opts.WithIdentity(jobKey).StoreDurably());
var jobKey5 = new JobKey(nameof(UpdateNextUserJob));
q.AddJob<UpdateNextUserJob>(opt => opt.WithIdentity(nameof(UpdateNextUserJob)));
q.AddTrigger(opt => opt.ForJob(jobKey5).WithCronSchedule("0 0 3 ? * * *"));
UpdateNextUserJob has a trigger and runs nightly. ExportHistoryJob is registered durably but
has no trigger, so it never fires on its own — it can only be started manually. Tracked as
R1-3.
Hangfire¶
Configured with SQL Server storage, PrepareSchemaIfNecessary = true and
services.AddHangfireServer(). It is genuinely used — four BackgroundJob.Enqueue calls in the
warehouse controllers (asset, destruction, report and stock exports; a fifth in ReportController is
commented out). Two BackgroundJob.Delete calls in BRFQ and BPurchaseRequest look like they
cancel an RFQ's scheduled job, but they are dead: they read RFQs.ScheduledJob, which no code ever
assigns, and nothing calls BackgroundJob.Schedule (R1-3).
No UseHangfireDashboard() is mapped, so there is no way to inspect, retry or cancel a failed
job from the application.
Custom hosted service¶
AddScheduledJobs() registers exactly one hosted service:
public static void AddScheduledJobs(this IServiceCollection service)
{
service.AddHostedService<BirthdayGreetingsJob>();
}
Infrastructure/LoanJob.cs implements the same pattern but is never registered, and its body
begins with #if DEBUG return; — so automatic loan settlement does not happen. Tracked as R7-4.
SignalR — real-time notifications¶
A hub (RealTime) is mapped at /hub, with CustomUserIdProvider implementing IUserIdProvider
so a message can be addressed to a specific employee. RealTimeNotifier is the BAL-side wrapper that
pushes updated notification counts to the top bar without a page refresh.
Declared but not used¶
| Dependency | Status |
|---|---|
AWS SNS (AWSSDK.) |
IAmazon is registered as a singleton factory in Startup. and resolved nowhere, so the factory never runs and no SNS client is ever built. It is dead registration, not a live connection. |
Refit + Refit. |
Zero Refit interfaces exist — no [Get("…")] / [Post("…")] attribute in the solution. |
| Twilio 5.59.0 | A single stray using Twilio. in Providers. No SMS is sent. |
| MailKit / MimeKit | Imported in Email, superseded by System.. |
Tracked together as R1-4.
Configuration summary¶
The connection strings, AWS settings and job settings come from appsettings.json (not committed;
appsettings.example.json shows the shape). A good deal is compiled in instead: the site URLs,
the mailbox addresses and passwords (SiteSettings.cs, R1-9), the SMTP host and port
(EmailManager.cs), the unused fixer.io key (BExchangeRate.cs) and the C:\upload paths. See
Deployment — Configuration for every key the application reads and
everything compiled in.
| Key | Purpose |
|---|---|
Connection |
Core database |
Connection |
Warehouse database |
Connection |
Hangfire job store |
Storage / client / client |
AWS credentials for S3 and SNS |
Storage |
Bucket and folder per feature — must cover all 19 Functionality values (R4-1) |
Sys |
Base URL used when building links in e-mails |
Jwt |
Not needed — read only if the JWT scheme is used, which it never is (R6-4) |
Scheduled |
Enable flag and cron for the hosted service |