API — Overview¶
The NKO ERP has no separate REST API layer. It is a server-rendered ASP.NET Core MVC application, so its HTTP surface is the set of controller actions: some return full pages, some return HTML fragments for modals, and a large minority return JSON to the jQuery/AJAX code in the views. This section documents every one of them — route, HTTP verb, parameters, permission key and response — grouped by business module.
Inventory
947 routable actions across 44 controller classes in three source folders —
NKO.Main/NKO.Main/Controllers/ (main), NKO.Main/NKO.Main/WarehouseControllers/ (warehouse),
and NKO.Lib/NKO.Shared/Controllers/ (shared). Counted directly from source; the per-module
pages below list each action individually.
Seven public static GetMonthDifference(DateTime, DateTime) helpers — on Advance,
HumanResource, Overtime, Payroll, Timesheet, Training and User — are not counted.
ASP.NET Core's action discovery (DefaultApplicationModelProvider.IsAction) rejects static
methods, so they are ordinary helpers, not endpoints. An earlier revision of this page listed
them as ungated routes; they are unreachable over HTTP.
| Module | Controllers | Actions |
|---|---|---|
| Administration & platform | 12 | 227 |
| Logistics | 10 | 174 |
| Procurement | 6 | 157 |
| HR | 3 | 115 |
| Payroll | 4 | 97 |
| Grant | 5 | 84 |
| Recruitment | 3 | 82 |
| Training | 1 | 11 |
| Total | 44 | 947 |
The gate counts under Authentication & authorization reconcile exactly against this total: 887 + 14 + 2 + 44 = 947.
SidebarViewComponent and TopbarViewComponent are not included: they derive from
ViewComponent, so their InvokeAsync methods are rendered from a layout rather than routed. See
Administration → View components.
Routing¶
A single conventional route is registered — there are no areas and (almost) no attribute routing:
// NKO.Main/NKO.Main/Startup.cs (line ~356)
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
So the route for an action is simply /{ControllerNameWithoutSuffix}/{ActionName}, with
/{Controller} alone hitting Index, and a single optional {id} segment. Everything else
arrives as a query string on GET or as form fields on POST.
/PurchaseRequest → PurchaseRequestController.Index
/PurchaseRequest/Details/{guid} → PurchaseRequestController.Details(string id)
/Stock/SRR?page=2 → StockController.SRR(int? page)
The four exceptions carry an explicit [Route] and are the only paths that look like a
conventional web API:
| Route | Controller | Auth | Purpose |
|---|---|---|---|
/ |
Files |
[Authorize] (class) |
Download a stored file by its storage key |
/ |
Files |
[Authorize] (class) |
Same, rendered inline as application/ |
/ |
Public |
none | Download by storage key, unauthenticated |
/ |
Public |
none | Inline PDF by storage key, unauthenticated |
Authentication & authorization¶
Sign-in is ASP.NET Core Identity (AccountController, ApplicationUser). Authorization is applied
per action, by one of three attributes — there is no global authorization filter and no
fallback policy, so an action with none of them is reachable by anyone.
| Gate | Count | Meaning |
|---|---|---|
[Claim |
887 | Requires sign-in and that the key is in the user's permission set (219 distinct keys in force). Two further copies of the attribute sit on the view components, where they are inert, and a 220th literal — GRNDelete — appears only inside a commented-out method in GRNReport. |
[Authorize] |
14 | Requires sign-in only — any employee passes |
[Allow |
2 | Explicitly public (/) |
| (no attribute) | 44 | Implicitly public — see Ungated actions |
ClaimRequirementFilter (NKO.Lib/NKO.Shared/Helpers/Sec/ClaimRequirementFilter.cs) runs as an
IAsyncAuthorizationFilter before the action:
- Not authenticated →
302to~/Account/Login?ReturnUrl={path}. - Authenticated, key missing →
ForbidResult, status set to403, then redirected to~/Error/UnAuthorised. - Authenticated, key present → populates
CurrentRequestData.PermissionsandCurrentRequestData.Employeefor the rest of the request, then the action runs.
The permission set is re-read from the database on every request
(PermissionsHandler.GetUserPermissions), so a group change or a disabled login takes effect on the
user's next click. Keys are free-form strings, conventionally "<Area><Action>" — HumanResourceIndex,
PurchaseOrderDetails, StockSRR. The special key General means "any authenticated employee".
Row-level scoping is not done by the filter. Actions that need it call
BUser.GetPermission(SharedPermissions.<Area>, userId) inside the body and narrow the query to
All / Location / Custom / Default. See Permissions.
Request conventions¶
- Verbs. 604 actions answer
GET, 343 answerPOST. NoPUT,DELETEorPATCHis used anywhere — deletes areGETorPOSTactions namedDelete…/Remove…. - Anti-forgery. 229 of the 343 POST actions carry
[ValidateAntiForgeryToken]; the remaining 114 do not (all of them behind a permission key). The public POSTs —/Account/Loginand the/Apps/*portals — are protected. - Identifiers. Most
Details/Edit/Deleteactions take a GUID string (string id), not the integer primary key; grids and lookups takeint id. - List parameters. Index actions share a paging/filter vocabulary:
int? page,string search_param,string searchString,string currentFilter,string sortOrder,int? filterYear,int? filterMonth,int? filterStatus,int? filterProject,int? filterBranch. - Model binding. POSTs bind either a full entity (
TrainingRequest model) or a view model (TrainingVM model) from form fields; AJAX POSTs usually take scalars.
The approval endpoint¶
Every multi-step document exposes one ContextActions POST that drives its whole state machine
instead of a verb-per-transition. There are 23 of them:
POST /{Document}/ContextActions
id document GUID
operationType which transition (…OpTypes constant)
userType which role is acting (…UserTypes constant)
idText free-text note / reason
The action resolves the constants from NKO.Lib/NKO.Shared/Helpers/CommonConstants.cs (warehouse:
NKO.Main/NKO.Main/Helpers/OperationConstants.cs), dispatches to the matching BAL Set…Accepted /
Set…Refused method, and redirects back to the document's Details page with a msg or
errorMsg query parameter. Endpoints: /Advance, /Asset (+ RegisterContextActions),
/Destruction, /Evaluation, /GRN, /GRNReport, /Jobs (+ ContextActionsApplicant,
TechnicalInterViewContextActions), /MaintenanceReq, /Overtime, /PaymentOrder, /Payroll,
/PurchaseOrder (+ JustificationContextActions), /PurchaseRequest, /RFQ (+ TechContextActions),
/Stock, /Timesheet (+ LeaveContextActions), /Training.
Most workflow transitions additionally require an approved e-signature — BSign.HasSign(userId)
is checked first and the transition is refused with "Please upload your E-signature." otherwise.
Not all of them, though. The check runs on Purchase Request, RFQ (both endpoints), Purchase
Order, GRN, Payment Order, and the warehouse Asset, Stock, GRN-report and Maintenance flows; Advance,
Overtime and Leave check the approver's PIN (VerifyPin) and the e-signature inside the business
class. It does not run on Payroll, Training, Jobs (all three endpoints), Evaluation,
Purchase Order justification or Destruction, and Timesheet checks it only when the requester
sends. See R3-9 in Recommendations.
Responses¶
Actions are not typed by response; the same Task<ActionResult> may return a page, a fragment or
JSON depending on the branch taken. Across all 947 actions, counting only live code — return
statements that are commented out are ignored:
| Response | Actions | Notes |
|---|---|---|
View(. |
379 | A full page rendered into _Layout (the public / pages use _App) |
Redirect / Redirect(. |
303 | Post-Redirect-Get, usually carrying msg / error |
Partial |
276 | HTML fragment loaded into a Bootstrap modal |
Json(. / new Json |
270 | AJAX payloads for grids, cascading dropdowns and validation |
File(. / File |
16 | Excel / PDF exports and attachment downloads |
Not |
10 | Missing storage object or record |
Content(. |
6 | Plain strings |
Bad / Status |
5 | Missing or invalid input — Human, Account and the two Ware look-ups |
Unauthorized() |
3 | A wrong E-signature PIN on Advance, Overtime and Leave approvals |
No action returns Forbid(); a missing permission key is turned into a 403 by
ClaimRequirementFilter before the action runs. Counts exceed 947 because most actions have several
return paths.
Partial views have no layout when requested directly
The 276 PartialView actions are meant to be fetched by AJAX into a modal. Opening one
directly by URL returns a bare, unstyled HTML fragment with no navigation — see R6-1 in
Recommendations.
Errors¶
There is no uniform error envelope. Behaviour by layer:
| Condition | Result |
|---|---|
| Not signed in, gated action | 302 → / |
| Signed in, permission key missing | 403 → redirect to / |
| Unhandled exception | The developer exception page — full stack trace, source excerpt and request data. Startup. calls Use unconditionally and registers no Use, so this is what production returns. See R1-1 in Recommendations. |
| Handled BAL failure | Redirect back to the list/detail view with ?error rendered as a banner |
| Workflow guard failed (wrong state, not the next user, no e-signature) | Same redirect + message; no distinct status code |
| Missing file in object storage | 404 from Not |
Because most failures are expressed as a redirect with a message rather than a status code, an HTTP client cannot distinguish success from a refused workflow transition by status alone.
Ungated actions¶
44 actions carry no authorization attribute at all. Some are public by design; others look accidental. They are listed in full on each module page (marked — open); the security-relevant ones are tracked as recommendations.
| Group | Actions | Assessment |
|---|---|---|
/ — public job-application, reference and supplier-quotation portals |
12 | By design. Unauthenticated, but they write Applicants and RFQProvider* rows. Anti-forgery is present; no rate limiting or CAPTCHA is. |
/ (GET + POST), / |
3 | By design. |
/, / |
2 | Unauthenticated file download by storage key, no ownership check — see R3-1. |
/ |
1 | A genuine endpoint. Linked from the "Sign out" control in Views/ and the shared topbar component. It returns plain Task rather than an IAction, so it does not look like an action at a glance — but it is one, and it is used on every sign-out. |
/, /, / |
3 | Not intended as endpoints. Internal helpers that happen to be public and non-static, so MVC routes them. Sign calls Password — see R3-7. |
/, /, /, /, / |
5 | Not intended as endpoints either — internal calculation helpers returning view models and double/Tuple values straight to the caller. See R3-7. |
/, /, / (4) |
6 | Stateless probes exposed as endpoints; the RFQ/ four leak workflow state to anonymous callers — see R3-3. |
/, /, /, / (2), /, /, /, /, /, /, /, / |
12 | Look unintentional — administrative, file-serving or data-returning actions with no gate. See R3-2. |
[AllowAnonymous] is counted separately
/Document/ExportationRotativeHeader and /Document/ExportationRotativeFooter carry an
explicit [AllowAnonymous], so they are deliberately public rather than ungated and are not
part of the 44 above.
Related¶
- Authentication · Permissions — how the gate behaves end to end.
- Security & Permissions · Request Pipeline.
- Modules — what each controller's screens are for.
- Recommendations — findings raised while writing this section.