Skip to content

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.json → StorageConfigs.region
clientId / clientSecret appsettings.json → StorageConfigs

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:

http://data.fixer.io/api/{date}?access_key={authKey}&base=TRY&symbols=USD,EUR

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 CommonSetting row 6 (EnableEmailService, set in General Settings) and send only if it parses as true. 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 an AppUser whose Email equals 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:

RotativaConfiguration.Setup(env.WebRootPath, "Rotativa");

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.cs in-memory UpdateNextUserJob (cron 0 0 3 ? * * * — daily 03:00); ExportHistoryJob
Hangfire 1.7.24 Startup.cs SQL Server (HangfireConnection) Fire-and-forget warehouse exports via BackgroundJob.Enqueue
Custom IHostedService AddScheduledJobsExtention none BirthdayGreetingsJob, 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.SimpleNotificationService) IAmazonSimpleNotificationService is registered as a singleton factory in Startup.cs 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.HttpClientFactory Zero Refit interfaces exist — no [Get("…")] / [Post("…")] attribute in the solution.
Twilio 5.59.0 A single stray using Twilio.Rest.Proxy.V1.Service.Session; in ProvidersController.cs. No SMS is sent.
MailKit / MimeKit Imported in EmailManager.cs, superseded by System.Net.Mail.

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
ConnectionStrings:HrConnection Core database
ConnectionStrings:WareConnection Warehouse database
ConnectionStrings:HangfireConnection Hangfire job store
StorageConfigs:region / clientId / clientSecret AWS credentials for S3 and SNS
StorageConfigs:Systems Bucket and folder per feature — must cover all 19 Functionality values (R4-1)
SysUrl Base URL used when building links in e-mails
JwtConfig:Secret Not needed — read only if the JWT scheme is used, which it never is (R6-4)
ScheduledJobsSettings:BirthdayGreetings* Enable flag and cron for the hosted service